Skip to main content

datui_lib/
sample_keys.rs

1//! The view's sample: `S` at the table, the draw that fills it as the table shows
2//! it, and taking it away.
3//!
4//! The sample is a step of the view, between the source and the query
5//! ([`crate::widgets::datatable::Sampled`]). Its rows are drawn off the UI thread by a
6//! [`Job::SampleDraw`], which keeps them in memory a chunk at a time
7//! ([`crate::table_sample`]); each chunk is laid under the view's frames as it lands,
8//! as a followed pipe's rows are.
9
10use crate::jobs::{Answer, Job, SampleDraw};
11use crate::sample_modal::SampleForm;
12use crate::table_sample::{Limit, MemoryCheck, MemoryProbe};
13use crate::widgets::datatable::DataTableState;
14use crate::{App, AppEvent, InputMode, form, sampling};
15use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
16use std::sync::Arc;
17
18/// What the status line says while a sample is drawn.
19const DRAWING: &str = "Sampling...";
20
21/// What Enter on a Sample form that edits the view's sample comes to.
22pub(crate) enum Submitted {
23    /// The form says why it cannot apply, or warns about memory, on its own line.
24    Stays,
25    /// No sample: take the view's away.
26    Clear,
27    /// Draw this sample; `anyway` past the memory warning, with no running check.
28    Draw {
29        sample: sampling::Sample,
30        anyway: bool,
31    },
32}
33
34impl App {
35    /// `S` at the table: the Sample form, on the view's sample, or on a new one.
36    pub(crate) fn open_table_sample_form(&mut self) {
37        let Some(state) = self.data_table_state.as_ref() else {
38            return;
39        };
40        let sample = match state.sampled() {
41            Some(sampled) => sampled.sample().clone(),
42            None => sampling::Sample {
43                rows: match self.app_config.analysis.sample_rows {
44                    0 => sampling::DEFAULT_SAMPLE_ROWS,
45                    rows => rows,
46                },
47                seed: crate::sample_modal::new_seed(),
48                ..sampling::Sample::default()
49            },
50        };
51        let context = self.sample_context(state.unsampled());
52        let mut form = SampleForm::new(&sample, context, &self.theme);
53        form.view = true;
54        form.bytes_per_row = Some(state.unsampled().sample_row_bytes(false));
55        form.source_bytes_per_row = Some(state.unsampled().sample_row_bytes(true));
56        self.sample_form = Some(form);
57        self.input_mode = InputMode::Sample;
58    }
59
60    /// Keys while the table's Sample form is open.
61    pub(crate) fn table_sample_form_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
62        self.sample_form.as_ref()?;
63        match self.sample_form_edit(event) {
64            form::FormKey::Cancel => {
65                self.sample_form = None;
66                self.input_mode = InputMode::Normal;
67            }
68            form::FormKey::Submit => match Self::submit_view_sample(
69                self.sample_form.as_mut()?,
70                MemoryCheck {
71                    limit: Limit::of_setting(self.app_config.analysis.sample_memory_limit),
72                    probe: Arc::clone(&self.memory_probe),
73                },
74            ) {
75                Submitted::Stays => {}
76                Submitted::Clear => {
77                    self.sample_form = None;
78                    self.input_mode = InputMode::Normal;
79                    self.clear_table_sample();
80                }
81                Submitted::Draw { sample, anyway } => {
82                    self.sample_form = None;
83                    self.input_mode = InputMode::Normal;
84                    self.apply_table_sample(sample, None, anyway, false);
85                }
86            },
87            _ => {}
88        }
89        None
90    }
91
92    /// A key on the table's Sample form, as the analysis form takes it: the field's
93    /// own keys, or what the form does next.
94    fn sample_form_edit(
95        &mut self,
96        event: &KeyEvent,
97    ) -> form::FormKey<crate::sample_modal::SampleField> {
98        let Some(form) = self.sample_form.as_mut() else {
99            return form::FormKey::Other;
100        };
101        let file_count = form.context.files.len();
102        let key = form::key(form, event);
103        match &key {
104            form::FormKey::Step(_, delta) => {
105                form.adjust(*delta > 0);
106                form.edited();
107            }
108            form::FormKey::Text(crate::sample_modal::SampleField::Files)
109                if matches!(event.code, KeyCode::PageDown | KeyCode::PageUp) =>
110            {
111                form.file_offset = if event.code == KeyCode::PageDown {
112                    (form.file_offset + crate::widgets::sample_form::FILES_SHOWN)
113                        .min(file_count.saturating_sub(1))
114                } else {
115                    form.file_offset
116                        .saturating_sub(crate::widgets::sample_form::FILES_SHOWN)
117                };
118            }
119            form::FormKey::Text(_) => {
120                if let Some(input) = form.input_mut(form.field) {
121                    let _ = input.handle_key(event, None);
122                }
123                form.edited();
124            }
125            _ => {}
126        }
127        key
128    }
129
130    /// Enter on a form that edits the view's sample. A sample whose estimate is more
131    /// than the memory there is to hold it warns on the form, naming the setting; a
132    /// second Enter draws it anyway, once.
133    pub(crate) fn submit_view_sample(form: &mut SampleForm, memory: MemoryCheck) -> Submitted {
134        if form.no_sample() {
135            return Submitted::Clear;
136        }
137        let sample = match form.finish() {
138            Ok(sample) => sample,
139            Err(error) => {
140                form.error = Some(error);
141                return Submitted::Stays;
142            }
143        };
144        if form.anyway {
145            return Submitted::Draw {
146                sample,
147                anyway: true,
148            };
149        }
150        if let Some(warning) = form.estimate().and_then(|bytes| memory.refuses(bytes)) {
151            form.error = Some(warning);
152            form.anyway = true;
153            return Submitted::Stays;
154        }
155        Submitted::Draw {
156            sample,
157            anyway: false,
158        }
159    }
160
161    /// The memory check a sample is drawn under: `analysis.sample_memory_limit`
162    /// against what is held, or the memory available now.
163    pub(crate) fn memory_check(&self) -> MemoryCheck {
164        MemoryCheck {
165            limit: Limit::of_setting(self.app_config.analysis.sample_memory_limit),
166            probe: Arc::clone(&self.memory_probe),
167        }
168    }
169
170    /// Read the memory available now from `probe` rather than from the system: for a
171    /// test that decides how much there is.
172    pub fn set_memory_probe(&mut self, probe: MemoryProbe) {
173        self.memory_probe = probe;
174    }
175
176    /// Draw `sample` as the view's sample, in place of any it has. The table shows
177    /// the rows as they land, with `replay` (the query, filters, sort and columns of
178    /// a view being applied) laid over them; with none, the view's own steps go back
179    /// on, unless the sample was drawn through them. `anyway` draws with no running
180    /// memory check. `then_analyze` runs Analysis's tool once it is drawn.
181    pub(crate) fn apply_table_sample(
182        &mut self,
183        sample: sampling::Sample,
184        replay: Option<crate::view::ViewSettings>,
185        anyway: bool,
186        then_analyze: bool,
187    ) {
188        self.draw_table_sample(sample, None, replay, anyway, then_analyze);
189    }
190
191    /// [`Self::apply_table_sample`], drawing a random sample of a stream the way
192    /// `path` says, as a view says it was drawn: the same rows again.
193    pub(crate) fn draw_table_sample(
194        &mut self,
195        sample: sampling::Sample,
196        path: Option<crate::table_sample::DrawPath>,
197        replay: Option<crate::view::ViewSettings>,
198        anyway: bool,
199        then_analyze: bool,
200    ) {
201        use crate::data_quality::QualityScope;
202        let Some(state) = self.data_table_state.as_ref() else {
203            return;
204        };
205        let source = state.unsampled();
206        // A view scope reads the view as shown: what it shows other than the source
207        // is what the sample stands for, so it is not laid on again. Its order is
208        // part of which rows a row range reads.
209        let reads_view = !sample.scope.uses_source();
210        let ranged = matches!(
211            sample.scope,
212            QualityScope::FirstRows(_) | QualityScope::ViewRows { .. }
213        );
214        let sorted = !source.get_sort_columns().is_empty() || !source.get_sort_ascending();
215        let through = reads_view
216            && (source.changes_rows() || !source.column_changes().is_empty() || ranged && sorted);
217        // The steps to lay on the new sample: a view's being applied; those on the
218        // sample it replaces; or the view's own, unless the sample stands for them.
219        let replay = replay.or_else(|| match state.sampled() {
220            Some(_) => Some(crate::view_settings_of(state)),
221            None => (!through).then(|| crate::view_settings_of(source)),
222        });
223        // A pivot is read whole once its rows are all there, which a sample being
224        // drawn is not: say so rather than leave it off.
225        if replay
226            .as_ref()
227            .is_some_and(|settings| settings.pivot.is_some())
228        {
229            self.error_modal.show(PIVOT_OVER_A_SAMPLE.to_string());
230            return;
231        }
232        self.put_down_sample_draw();
233        let Some(state) = self.data_table_state.as_ref() else {
234            return;
235        };
236        let source = state.unsampled();
237        let (cut, known_total) = Self::table_sample_source(source, &sample.scope);
238        // How a random sample of a stream is drawn is decided once for what it is
239        // drawn from: the view's word, the way it was drawn here before, or by
240        // whether the count is in. The same seed keeps the same rows either way.
241        let path_key = Self::sample_path_key(source, &sample.scope);
242        let path = (sample.method == sampling::SampleMethod::Spread).then(|| {
243            path.or_else(|| {
244                self.sample_paths
245                    .iter()
246                    .find(|(key, _)| *key == path_key)
247                    .map(|(_, path)| *path)
248            })
249            .unwrap_or(match known_total {
250                Some(of) => crate::table_sample::DrawPath::Bernoulli { of },
251                None => crate::table_sample::DrawPath::Reservoir,
252            })
253        });
254        let bytes_per_row = Some(source.sample_row_bytes(sample.scope.uses_source()));
255        let streaming = self.app_config.performance.streaming;
256        let rows = Arc::new(crate::table_sample::SampleRows::default());
257        let (memory, watch) = if anyway {
258            (MemoryCheck::off(), sampling::ReadWatch::default())
259        } else {
260            let memory = self.memory_check();
261            // A sampler that keeps its rows to the end is checked as it holds them.
262            let held = memory.clone();
263            let watch = sampling::ReadWatch::judging_held(Arc::new(move |bytes, rows| {
264                held.holds_too_much(bytes, rows)
265            }));
266            (memory, watch)
267        };
268        let job = Job::SampleDraw(Box::new(SampleDraw {
269            sample: sample.clone(),
270            rows: Arc::clone(&rows),
271            watch: watch.clone(),
272            through,
273            replay,
274            then_analyze,
275            path,
276            path_key,
277            schema: None,
278        }));
279        self.spawn_job(job, Some(DRAWING), move |worker| {
280            let report = worker.reporter();
281            let failed = |error: color_eyre::eyre::Report| {
282                crate::error_display::user_message_from_report(&error, None)
283            };
284            let lf = cut.cut(&sample.scope).map_err(failed)?;
285            let schema = lf.clone().collect_schema().map_err(|e| failed(e.into()))?;
286            report(crate::Progress::SampleBegun(schema));
287            let live = crate::table_sample::Live {
288                rows,
289                notify: Arc::new(move || report(crate::Progress::SampleGrew)),
290                memory,
291                watch,
292                bytes_per_row,
293            };
294            let drawn =
295                crate::table_sample::draw(&lf, &sample, known_total, path, streaming, &live)
296                    .map_err(failed)?;
297            Ok(Answer::SampleDrawn(drawn))
298        });
299    }
300
301    /// What a draw from `scope` of `source` is remembered by: the scope, and the
302    /// view's steps it reads through.
303    fn sample_path_key(
304        source: &DataTableState,
305        scope: &crate::data_quality::QualityScope,
306    ) -> String {
307        let settings = crate::view::ViewSettings {
308            sample: None,
309            chart: None,
310            ..crate::view_settings_of(source)
311        };
312        let steps = if scope.uses_source() {
313            String::new()
314        } else {
315            serde_json::to_string(&settings).unwrap_or_default()
316        };
317        format!("{}\n{steps}", scope.command())
318    }
319
320    /// Where a view's sample is drawn from: the loaded source for a source scope,
321    /// otherwise the view as shown (in its order for a row range), with every column
322    /// the view has, whole: the sample is the view's rows, not an analysis's.
323    fn table_sample_source(
324        state: &DataTableState,
325        scope: &crate::data_quality::QualityScope,
326    ) -> (sampling::SampleSource, Option<usize>) {
327        use crate::data_quality::QualityScope;
328        if scope.uses_source() {
329            let (lf, source) = state.data_quality_source_scan();
330            return (sampling::SampleSource::loaded(lf, source), None);
331        }
332        let lf = match scope {
333            QualityScope::FirstRows(_) | QualityScope::ViewRows { .. } => state.lf().clone(),
334            _ => state.analysis_lf(),
335        };
336        let columns: Vec<polars::prelude::Expr> = state
337            .schema()
338            .iter_names()
339            .map(|name| polars::prelude::col(name.clone()))
340            .collect();
341        (
342            sampling::SampleSource::view(lf.select(columns)),
343            sampling::view_scope_rows(state.num_rows_if_valid(), scope),
344        )
345    }
346
347    /// The view's sample goes: the view it was drawn from comes back, with the
348    /// query, filters and sort laid on the sample laid on it instead, unless the
349    /// sample was drawn through the view's own.
350    pub(crate) fn clear_table_sample(&mut self) {
351        let Some(sampled) = self.data_table_state.as_ref().and_then(|s| s.sampled()) else {
352            return;
353        };
354        let through = sampled.through();
355        let settings = self.data_table_state.as_ref().map(crate::view_settings_of);
356        // A pivot over the sample would have to read the whole source to move onto
357        // it: the sample stays, and the way out is said.
358        if !through && settings.as_ref().is_some_and(|s| s.pivot.is_some()) {
359            self.error_modal.show(PIVOT_OFF_A_SAMPLE.to_string());
360            return;
361        }
362        self.put_down_sample_draw();
363        let Some(state) = self.data_table_state.take() else {
364            return;
365        };
366        let mut source = state.into_unsampled();
367        if !through && let Some(settings) = settings {
368            let laid = source.deferred(|s| {
369                s.reset_view_for_replay();
370                Self::replay_view(s, &settings, None).map(|_| ())
371            });
372            if let Err(error) = laid {
373                self.error_modal.show(format!(
374                    "The view's steps did not go back on the source: {error}"
375                ));
376            }
377        }
378        self.data_table_state = Some(source);
379        self.sample_changed();
380        self.flash_note("Sample cleared".to_string());
381        self.spawn_async_collect(Self::LOADING_BUFFER);
382    }
383
384    /// The view's rows changed underneath every result read from them: Analysis's,
385    /// the chart's and the rows a page read held.
386    pub(crate) fn sample_changed(&mut self) {
387        self.forget_the_rows_read();
388        self.chart_cache.clear();
389        let modal = &mut self.analysis_modal;
390        modal.describe_results = None;
391        modal.distribution_results = None;
392        modal.correlation_results = None;
393        modal.data_quality_results = None;
394        modal.data_quality_last_plan = None;
395        let sampled = self
396            .data_table_state
397            .as_ref()
398            .is_some_and(|state| state.sampled().is_some());
399        self.analysis_modal.follow_view_sample(sampled);
400        self.sync_quality_plan();
401    }
402
403    /// The draw in flight, if one is: its record.
404    pub(crate) fn sample_draw(&self) -> Option<&SampleDraw> {
405        match self.jobs.current(|job| matches!(job, Job::SampleDraw(_))) {
406            Some((_, Job::SampleDraw(draw))) => Some(draw),
407            _ => None,
408        }
409    }
410
411    /// Whether the view's sample is being drawn.
412    pub fn sample_drawing(&self) -> bool {
413        self.sample_draw().is_some()
414    }
415
416    /// Esc while the sample is drawn: it stops, and the rows so far stay.
417    pub(crate) fn stop_sample_draw(&mut self) {
418        if let Some(draw) = self.sample_draw() {
419            draw.watch.stop();
420        }
421    }
422
423    /// Stop the draw in flight and drop whatever it still sends: another sample
424    /// replaces it, or the view it was for goes.
425    pub(crate) fn put_down_sample_draw(&mut self) {
426        if let Some(draw) = self.sample_draw() {
427            draw.watch.stop();
428        }
429        self.jobs.supersede(|job| matches!(job, Job::SampleDraw(_)));
430    }
431
432    /// Whether the view on screen is the one the draw in flight fills.
433    fn draw_fills_view(&self, draw: &SampleDraw) -> bool {
434        self.data_table_state
435            .as_ref()
436            .and_then(|state| state.sampled())
437            .is_some_and(|sampled| sampled.holds(&draw.rows))
438    }
439
440    /// The draw has cut its rows to their scope: what their columns are is kept
441    /// for the view that takes its first rows.
442    pub(crate) fn sample_begun(&mut self, schema: &polars::prelude::SchemaRef) {
443        if let Some(Job::SampleDraw(draw)) = self
444            .jobs
445            .current_mut(|job| matches!(job, Job::SampleDraw(_)))
446        {
447            draw.schema = Some(schema.clone());
448        }
449    }
450
451    /// The view becomes the sample's, with the steps laid on it, in place of the
452    /// view it is drawn from or the sample it replaces. Only once rows have come:
453    /// a draw that fails or stops first leaves the view as it was. Returns whether
454    /// the view is the draw's.
455    fn take_on_sample(&mut self, draw: &SampleDraw) -> bool {
456        if self.draw_fills_view(draw) {
457            return true;
458        }
459        let Some(schema) = draw.schema.as_ref() else {
460            return false;
461        };
462        let Some(state) = self.data_table_state.take() else {
463            return false;
464        };
465        let source = state.into_unsampled();
466        let mut view = match DataTableState::sampled_from(
467            source,
468            draw.sample.clone(),
469            schema,
470            Arc::clone(&draw.rows),
471            draw.through,
472            draw.path,
473        ) {
474            Ok(view) => view,
475            Err(error) => {
476                self.put_down_sample_draw();
477                self.error_modal
478                    .show(format!("Cannot show the sample: {error}"));
479                return false;
480            }
481        };
482        if let Some(settings) = &draw.replay {
483            let laid = view.deferred(|s| Self::replay_view(s, settings, None));
484            match laid {
485                Ok(crate::Replayed::Planned) => {}
486                // Refused before the draw started; never left off silently.
487                Ok(crate::Replayed::Pivot(_)) => {
488                    self.error_modal.show(PIVOT_OVER_A_SAMPLE.to_string());
489                }
490                Err(error) => self.flash_note(format!(
491                    "The view's steps did not apply to the sample: {error}"
492                )),
493            }
494        }
495        self.data_table_state = Some(view);
496        self.sample_changed();
497        true
498    }
499
500    /// The draw kept another chunk: the view reads it, staying where it is.
501    pub(crate) fn sample_grew(&mut self) {
502        let Some(draw) = self.sample_draw().cloned() else {
503            return;
504        };
505        if !self.take_on_sample(&draw) {
506            return;
507        }
508        let grew = self
509            .data_table_state
510            .as_mut()
511            .and_then(DataTableState::sample_grew);
512        // Rows read through the frame before it grew are of fewer rows, in another
513        // order: nothing may land them now.
514        if grew == Some(false) {
515            self.forget_the_rows_read();
516        }
517        // The page is read again when nothing else is reading it; the next chunk, or
518        // the end, reads it otherwise.
519        if grew.is_some() && self.rows_in_flight().is_none() && self.in_normal_table_view() {
520            self.spawn_collect(None);
521        }
522    }
523
524    /// The draw ended: the rows go into source order, and a run Analysis waits for
525    /// starts. A draw memory stopped says why.
526    pub(crate) fn sample_drawn(
527        &mut self,
528        job: Job,
529        current: bool,
530        drawn: crate::table_sample::Drawn,
531    ) -> Option<AppEvent> {
532        let Job::SampleDraw(draw) = job else {
533            return None;
534        };
535        if !current || !self.take_on_sample(&draw) {
536            return None;
537        }
538        if let Some(path) = drawn.path {
539            self.sample_paths.retain(|(key, _)| *key != draw.path_key);
540            self.sample_paths.push((draw.path_key.clone(), path));
541        }
542        if let Some(state) = self.data_table_state.as_mut() {
543            state.sample_drawn(drawn);
544        }
545        self.forget_the_rows_read();
546        if let Some(reason) = draw.rows.stopped() {
547            self.flash_note(reason);
548        }
549        self.spawn_collect(None);
550        if draw.then_analyze && self.analysis_modal.active {
551            self.analysis_modal.computing = None;
552            return self.start_analysis_run();
553        }
554        None
555    }
556
557    /// The draw failed, or was stopped before it kept a row. The view stays as it
558    /// was when no row had come; rows that had stay, as a sample cut short.
559    pub(crate) fn sample_draw_failed(&mut self, job: &Job, current: bool, message: &str) {
560        let Job::SampleDraw(draw) = job else {
561            return;
562        };
563        if !current {
564            return;
565        }
566        if self.draw_fills_view(draw)
567            && let Some(state) = self.data_table_state.as_mut()
568        {
569            state.sample_drawn(crate::table_sample::Drawn {
570                cut: true,
571                path: draw.path,
572                ..Default::default()
573            });
574            self.forget_the_rows_read();
575            self.spawn_collect(None);
576        }
577        if draw.then_analyze {
578            self.analysis_modal.computing = None;
579        }
580        if message == sampling::CANCELLED {
581            self.flash_note("Sample stopped".to_string());
582        } else {
583            self.error_modal.show(message.to_string());
584        }
585    }
586
587    /// While a sample is drawn, the keys that read only the rows on hand act at
588    /// once: moving, finding, inspecting, and every key of the find line, the
589    /// inspector, the Info panel and the help. Anything that needs every row
590    /// (a sort, a query, Analysis, a chart, an export) waits for the sample.
591    pub(crate) fn key_acts_while_sampling(&self, key: &KeyEvent) -> bool {
592        if self.busy
593            || self.loading.waits()
594            || !self
595                .jobs
596                .keys_held_only_by(|job| matches!(job, Job::SampleDraw(_)))
597        {
598            return false;
599        }
600        match self.input_mode {
601            InputMode::Inspect | InputMode::Info => return true,
602            InputMode::Editing if self.input_type == Some(crate::InputType::Find) => return true,
603            InputMode::Normal if self.help.is_open() => return true,
604            InputMode::Normal if self.in_normal_table_view() => {}
605            _ => return false,
606        }
607        let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
608        match key.code {
609            KeyCode::Up
610            | KeyCode::Down
611            | KeyCode::PageUp
612            | KeyCode::PageDown
613            | KeyCode::Home
614            | KeyCode::End
615            | KeyCode::Char(' ') => true,
616            KeyCode::Char('f' | 'b' | 'd' | 'u') if ctrl => true,
617            KeyCode::Char('j' | 'k' | 'G' | '/' | 'f' | 'n' | 'N' | 'g' | 'i') => !ctrl,
618            KeyCode::Enter => self.enter_inspects(),
619            _ => false,
620        }
621    }
622
623    /// What a chart says under its plot about the rows it drew. A view with a sample
624    /// is charted from all of it: the note says which sample, with its seed, so the
625    /// chart can be drawn again.
626    pub(crate) fn chart_notes_of(
627        &self,
628        prepared: &crate::chart_jobs::ChartPrepared,
629        middot: &str,
630    ) -> Vec<String> {
631        let mut notes = prepared.notes(middot);
632        let sampled = self.data_table_state.as_ref().and_then(|s| s.sampled());
633        if let Some(sampled) = sampled {
634            let mut note = sampled.label();
635            if matches!(
636                sampled.sample().method,
637                sampling::SampleMethod::Spread | sampling::SampleMethod::PerPartition { .. }
638            ) {
639                note.push_str(&format!(" {middot} seed {}", sampled.sample().seed));
640            }
641            notes.insert(0, note);
642        }
643        notes
644    }
645
646    /// Apply `view`, whose rows are a sample: the view goes back to its source, the
647    /// view the sample was drawn through goes on, and the sample is drawn again from
648    /// its seed, the way it was. The view's own steps go on the sample as it arrives.
649    pub(crate) fn apply_sampled_view(
650        &mut self,
651        view: &crate::view::SavedView,
652        saved: &crate::view::SavedSample,
653        why: Option<crate::view::MatchReason>,
654    ) -> color_eyre::Result<()> {
655        let sample = saved.sample()?;
656        let through = saved.through.as_deref();
657        if through.is_some_and(|through| through.pivot.is_some()) || view.settings.pivot.is_some() {
658            return Err(color_eyre::eyre::eyre!("{PIVOT_OVER_A_SAMPLE}"));
659        }
660        self.put_down_sample_draw();
661        let Some(state) = self.data_table_state.take() else {
662            return Ok(());
663        };
664        let mut source = state.into_unsampled();
665        let replayed = source.try_transition(|s| {
666            s.reset_view_for_replay();
667            match through {
668                Some(through) => Self::replay_view(s, through, None).map(|_| ()),
669                None => Ok(()),
670            }
671        });
672        self.data_table_state = Some(source);
673        replayed?;
674        self.sample_changed();
675        if let Some(path) = &self.path {
676            use crate::logging::LogFailure;
677            self.view_manager
678                .record_use(&view.id, path)
679                .or_log("record a view's use");
680        }
681        self.active_view_id = Some(view.id.clone());
682        self.restore_view_chart(view.settings.chart.as_ref());
683        let mut settings = view.settings.clone();
684        settings.sample = None;
685        self.draw_table_sample(sample, saved.path, Some(settings), false, false);
686        if let Some(why) = why {
687            self.flash_view_applied(&view.name, why);
688        }
689        self.first_rows_settled();
690        Ok(())
691    }
692}
693
694/// Why a sample is not drawn under a pivot.
695const PIVOT_OVER_A_SAMPLE: &str = "A pivot cannot be laid on a sample as it is drawn: sample \
696     the pivoted view (Rows from: All rows), or take the pivot away with R";
697
698/// Why the sample under a pivot stays.
699const PIVOT_OFF_A_SAMPLE: &str = "The pivot on the sample cannot move to the source: R takes \
700     away both";