1use crate::chart::chart_data::{BarOrder, ValueRange};
14use crate::widgets::ui::PickerState;
15use polars::prelude::DataType;
16use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum Mark {
22 #[default]
23 Line,
24 Scatter,
25 Bar,
26 Histogram,
27 Box,
28 Kde,
29 Heatmap,
30}
31
32impl Mark {
33 pub const ALL: [Self; 7] = [
34 Self::Line,
35 Self::Scatter,
36 Self::Bar,
37 Self::Histogram,
38 Self::Box,
39 Self::Kde,
40 Self::Heatmap,
41 ];
42
43 pub fn label(self) -> &'static str {
44 match self {
45 Self::Line => "Line",
46 Self::Scatter => "Scatter",
47 Self::Bar => "Bar",
48 Self::Histogram => "Histogram",
49 Self::Box => "Box",
50 Self::Kde => "KDE",
51 Self::Heatmap => "Heatmap",
52 }
53 }
54
55 pub fn is_xy(self) -> bool {
57 matches!(self, Self::Line | Self::Scatter)
58 }
59}
60
61#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum TimeUnit {
65 #[default]
66 None,
67 Day,
68 Week,
69 Month,
70 Quarter,
71 Year,
72}
73
74impl TimeUnit {
75 pub const ALL: [Self; 6] = [
76 Self::None,
77 Self::Day,
78 Self::Week,
79 Self::Month,
80 Self::Quarter,
81 Self::Year,
82 ];
83
84 pub fn label(self) -> &'static str {
85 match self {
86 Self::None => "none",
87 Self::Day => "day",
88 Self::Week => "week",
89 Self::Month => "month",
90 Self::Quarter => "quarter",
91 Self::Year => "year",
92 }
93 }
94
95 pub fn every(self) -> Option<&'static str> {
97 match self {
98 Self::None => None,
99 Self::Day => Some("1d"),
100 Self::Week => Some("1w"),
101 Self::Month => Some("1mo"),
102 Self::Quarter => Some("1q"),
103 Self::Year => Some("1y"),
104 }
105 }
106}
107
108#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "lowercase")]
112pub enum Aggregate {
113 #[default]
114 None,
115 Count,
116 Distinct,
118 Sum,
119 Mean,
120 Median,
121 Stdev,
124 Quantile,
126 Min,
127 Max,
128 First,
131 Last,
132}
133
134impl Aggregate {
135 pub const ALL: [Self; 12] = [
136 Self::None,
137 Self::Count,
138 Self::Distinct,
139 Self::Sum,
140 Self::Mean,
141 Self::Median,
142 Self::Stdev,
143 Self::Quantile,
144 Self::Min,
145 Self::Max,
146 Self::First,
147 Self::Last,
148 ];
149
150 pub fn label(self) -> &'static str {
151 match self {
152 Self::None => "none",
153 Self::Count => "count",
154 Self::Distinct => "distinct",
155 Self::Sum => "sum",
156 Self::Mean => "mean",
157 Self::Median => "median",
158 Self::Stdev => "stdev",
159 Self::Quantile => "quantile",
160 Self::Min => "min",
161 Self::Max => "max",
162 Self::First => "first",
163 Self::Last => "last",
164 }
165 }
166
167 pub fn named(self, quantile: u8) -> String {
170 match self {
171 Self::Quantile => format!("p{quantile}"),
172 other => other.label().to_string(),
173 }
174 }
175
176 pub fn follows_row_order(self) -> bool {
178 matches!(self, Self::First | Self::Last)
179 }
180
181 pub fn is_fractional(self) -> bool {
183 matches!(
184 self,
185 Self::Mean | Self::Median | Self::Stdev | Self::Quantile
186 )
187 }
188
189 pub fn takes_any_y(self) -> bool {
192 self == Self::Distinct
193 }
194
195 pub fn runs_cumulative(self) -> bool {
200 matches!(
201 self,
202 Self::Count | Self::Sum | Self::Mean | Self::Median | Self::Min | Self::Max
203 )
204 }
205
206 pub fn is_count(self) -> bool {
208 matches!(self, Self::Count | Self::Distinct)
209 }
210}
211
212#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "lowercase")]
215pub enum Cumulative {
216 #[default]
217 Off,
218 Sum,
220 Compound,
223}
224
225impl Cumulative {
226 pub const ALL: [Self; 3] = [Self::Off, Self::Sum, Self::Compound];
227
228 pub fn label(self) -> &'static str {
229 match self {
230 Self::Off => "off",
231 Self::Sum => "running sum",
232 Self::Compound => "compound",
233 }
234 }
235}
236
237#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct XEncoding {
240 pub field: Option<String>,
241 pub time_unit: TimeUnit,
242}
243
244#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct YEncoding {
247 pub field: Vec<String>,
249 pub aggregate: Aggregate,
250 pub cumulative: Cumulative,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub percentile: Option<u8>,
254}
255
256pub const QUANTILES: [u8; 8] = [1, 5, 10, 25, 75, 90, 95, 99];
258
259pub const QUANTILE_DEFAULT: u8 = 90;
261
262impl YEncoding {
263 pub fn quantile(&self) -> u8 {
265 self.percentile.unwrap_or(QUANTILE_DEFAULT)
266 }
267
268 pub fn aggregate_name(&self) -> String {
270 self.aggregate.named(self.quantile())
271 }
272}
273
274#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct ColorEncoding {
277 pub field: Option<String>,
278 pub values: Vec<Option<String>>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub other: Option<bool>,
285}
286
287#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
288pub struct Encoding {
289 pub x: XEncoding,
290 pub y: YEncoding,
291 pub color: ColorEncoding,
292}
293
294#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
296pub struct ChartSpec {
297 pub mark: Mark,
298 pub encoding: Encoding,
299}
300
301pub const COLOR_MAX: usize = 10;
305
306pub const Y_SERIES_MAX: usize = COLOR_MAX;
308
309pub const HISTOGRAM_DEFAULT_BINS: usize = 40;
311pub const HISTOGRAM_MIN_BINS: usize = 5;
312pub const HISTOGRAM_MAX_BINS: usize = 100;
313
314pub const HEATMAP_DEFAULT_BINS: usize = 20;
316pub const HEATMAP_MIN_BINS: usize = 5;
317pub const HEATMAP_MAX_BINS: usize = 60;
318
319pub const KDE_BANDWIDTH_MIN: f64 = 0.2;
321pub const KDE_BANDWIDTH_MAX: f64 = 5.0;
322pub const KDE_BANDWIDTH_STEP: f64 = 0.1;
323
324pub const CHART_ROW_LIMIT_MAX: usize = u32::MAX as usize;
326
327#[derive(Debug, Clone, Default, PartialEq, Eq)]
330pub struct RowsDraft {
331 pub every: bool,
333 pub typed: Option<String>,
335 pub error: Option<&'static str>,
337}
338
339#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
341pub enum ChartFocus {
342 #[default]
344 Type,
345 X,
347 TimeUnit,
349 Order,
351 Bins,
353 Y,
355 Aggregate,
357 Quantile,
359 Color,
361 ColorValues,
363 Cumulative,
365 Bandwidth,
366 Range,
367 YStartsAtZero,
368 LogScale,
369 ShowLegend,
370 Grid,
371 LimitRows,
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub enum ShelfUse {
378 Used,
380 Dimmed(&'static str),
382}
383
384#[derive(Clone, Copy, Default)]
386pub struct ChartColumns<'a> {
387 pub numeric: &'a [String],
388 pub datetime: &'a [String],
389 pub bucketable: &'a [String],
391 pub category: &'a [String],
394}
395
396#[derive(Debug, Clone, Default, PartialEq)]
399pub struct ColorCounts {
400 pub column: String,
401 pub values: Vec<(Option<String>, u64)>,
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub enum PickerFor {
407 X,
408 Y,
409 Color,
410 ColorValues,
411}
412
413pub const NONE_ITEM: &str = "none";
415
416#[derive(Default)]
418pub struct ChartModal {
419 pub spec: ChartSpec,
420 pub series_cap: Option<usize>,
423 pub row_order: Option<String>,
426 pub suggested: Option<String>,
429 pub y_starts_at_zero: bool,
430 pub log_scale: bool,
431 pub show_legend: bool,
432 pub grid: bool,
434 pub share: bool,
436 pub hist_bins: usize,
437 pub heatmap_bins: usize,
438 pub kde_bandwidth_factor: f64,
439 pub value_range: ValueRange,
441 pub bar_order: BarOrder,
442 pub row_limit: Option<usize>,
445 pub sample_rows: usize,
447 pub rows_draft: Option<RowsDraft>,
449 pub view_rows: Option<usize>,
451 pub view_sampled: bool,
454 pub restored: bool,
457 pub focus: ChartFocus,
458 pub picker: Option<PickerState>,
460 pub picker_for: Option<PickerFor>,
461 pub picker_details: Vec<String>,
463 pub numeric_candidates: Vec<String>,
464 pub temporal_candidates: Vec<String>,
465 pub bucketable_candidates: Vec<String>,
467 pub category_candidates: Vec<String>,
468 pub color_counts: Option<ColorCounts>,
470 pub dataset: Option<u64>,
474 pub opened_on: Option<String>,
475 pub units: Vec<(String, String)>,
478 pub plot_focus: bool,
480 pub cursor_x: Option<f64>,
483 pub plot: Option<crate::widgets::crosshair::PlotPlace>,
486}
487
488impl ChartModal {
489 pub fn new() -> Self {
490 Self::default()
491 }
492
493 pub fn series_max(&self) -> usize {
496 self.series_cap.unwrap_or(COLOR_MAX).clamp(1, COLOR_MAX)
497 }
498
499 pub fn axis_title(&self, column: &str) -> String {
501 match self.units.iter().find(|(name, _)| name == column) {
502 Some((_, unit)) => format!("{column} ({unit})"),
503 None => column.to_string(),
504 }
505 }
506
507 pub fn mark(&self) -> Mark {
508 self.spec.mark
509 }
510
511 pub fn x(&self) -> Option<&String> {
512 self.spec.encoding.x.field.as_ref()
513 }
514
515 pub fn y(&self) -> &[String] {
516 &self.spec.encoding.y.field
517 }
518
519 pub fn aggregate(&self) -> Aggregate {
520 self.spec.encoding.y.aggregate
521 }
522
523 pub fn color(&self) -> Option<&String> {
524 self.spec.encoding.color.field.as_ref()
525 }
526
527 pub fn open(
532 &mut self,
533 columns: ChartColumns<'_>,
534 cursor: Option<(&str, &DataType)>,
535 default_row_limit: Option<usize>,
536 grid: bool,
537 dataset: u64,
538 ) {
539 self.close_picker();
540 self.temporal_candidates = columns.datetime.to_vec();
541 self.bucketable_candidates = columns.bucketable.to_vec();
542 self.numeric_candidates = columns.numeric.to_vec();
543 self.category_candidates = columns.category.to_vec();
544 self.plot_focus = false;
545 self.rows_draft = None;
546 let opened_on = cursor.map(|(name, _)| name.to_string());
547 let restored = std::mem::take(&mut self.restored);
548 if self.dataset == Some(dataset) && (self.opened_on == opened_on || restored) {
549 self.keep_existing_choices();
550 self.settle();
551 self.focus = ChartFocus::Type;
552 return;
553 }
554 self.dataset = Some(dataset);
555 self.opened_on = opened_on;
556 self.cursor_x = None;
557 self.color_counts = None;
558 self.spec = ChartSpec::default();
559 self.suggested = None;
560 self.y_starts_at_zero = false;
561 self.log_scale = false;
562 self.show_legend = true;
563 self.grid = grid;
564 self.share = false;
565 self.value_range = ValueRange::All;
566 self.row_limit = default_row_limit.and_then(|n| {
567 if n == 0 {
568 None
569 } else {
570 Some(n.clamp(1, CHART_ROW_LIMIT_MAX))
571 }
572 });
573 self.sample_rows = self
574 .row_limit
575 .unwrap_or(crate::config::DEFAULT_CHART_ROW_LIMIT);
576 self.hist_bins = HISTOGRAM_DEFAULT_BINS;
577 self.kde_bandwidth_factor = 1.0;
578 self.heatmap_bins = HEATMAP_DEFAULT_BINS;
579 self.bar_order = BarOrder::Value;
580 if let Some((name, dtype)) = cursor {
581 self.suggest(name, dtype);
582 }
583 self.focus = ChartFocus::Type;
584 }
585
586 pub fn restore(&mut self, saved: &crate::view::SavedChart, dataset: u64) {
588 self.spec = saved.spec.clone();
589 self.hist_bins = saved.histogram_bins.max(1);
590 self.heatmap_bins = saved.heatmap_bins.max(1);
591 self.kde_bandwidth_factor = saved.bandwidth;
592 self.value_range = saved.range;
593 self.bar_order = saved.bar_order;
594 self.share = saved.share;
595 self.y_starts_at_zero = saved.y_starts_at_zero;
596 self.log_scale = saved.log_scale;
597 self.show_legend = saved.legend;
598 self.grid = saved.grid;
599 self.row_limit = saved.rows;
600 if let Some(rows) = saved.rows {
601 self.sample_rows = rows;
602 }
603 self.suggested = None;
604 self.color_counts = None;
605 self.cursor_x = None;
606 self.dataset = Some(dataset);
607 self.restored = true;
608 }
609
610 pub fn saved(
613 &self,
614 seed: u64,
615 export: Option<crate::view::SavedChartExport>,
616 ) -> crate::view::SavedChart {
617 crate::view::SavedChart {
618 spec: self.spec.clone(),
619 histogram_bins: self.hist_bins,
620 heatmap_bins: self.heatmap_bins,
621 bandwidth: self.kde_bandwidth_factor,
622 range: self.value_range,
623 bar_order: self.bar_order,
624 share: self.share,
625 y_starts_at_zero: self.y_starts_at_zero,
626 log_scale: self.log_scale,
627 legend: self.show_legend,
628 grid: self.grid,
629 rows: if self.view_sampled {
630 None
631 } else {
632 self.row_limit
633 },
634 seed: (!self.view_sampled && self.row_limit.is_some()).then_some(seed),
635 export,
636 }
637 }
638
639 fn suggest(&mut self, name: &str, dtype: &DataType) {
643 let name = name.to_string();
644 let encoding = &mut self.spec.encoding;
645 if self.temporal_candidates.contains(&name) {
646 self.spec.mark = Mark::Line;
647 encoding.x.field = Some(name);
648 if let Some(y) = self.numeric_candidates.first() {
649 encoding.y.field = vec![y.clone()];
650 }
651 } else if dtype.is_float() || (dtype.is_numeric() && !dtype.is_integer()) {
652 self.spec.mark = Mark::Histogram;
653 encoding.x.field = Some(name);
654 } else if self.category_candidates.contains(&name) && !dtype.is_integer() {
655 self.spec.mark = Mark::Bar;
656 encoding.x.field = Some(name);
657 encoding.y.aggregate = Aggregate::Count;
658 } else if self.numeric_candidates.contains(&name) {
659 self.spec.mark = Mark::Histogram;
661 encoding.x.field = Some(name);
662 } else {
663 return;
664 }
665 self.suggested = Some(crate::formats::column_types::dtype_label(dtype));
666 }
667
668 pub fn close(&mut self) {
670 self.close_picker();
671 self.plot_focus = false;
672 self.rows_draft = None;
673 }
674
675 pub fn close_picker(&mut self) {
676 self.picker = None;
677 self.picker_for = None;
678 self.picker_details.clear();
679 }
680
681 pub fn has_crosshair(&self) -> bool {
684 self.spec.mark.is_xy() && self.plot.is_some()
685 }
686
687 fn keep_existing_choices(&mut self) {
689 let mut all: Vec<&String> = self.numeric_candidates.iter().collect();
690 all.extend(self.temporal_candidates.iter());
691 all.extend(self.category_candidates.iter());
692 let has = |c: &String| all.contains(&c);
693 let encoding = &mut self.spec.encoding;
694 if encoding.x.field.as_ref().is_some_and(|c| !has(c)) {
695 encoding.x.field = None;
696 }
697 encoding.y.field.retain(|c| has(c));
698 if encoding.color.field.as_ref().is_some_and(|c| !has(c)) {
699 encoding.color.field = None;
700 encoding.color.values.clear();
701 }
702 }
703
704 pub fn x_is_bucketable(&self) -> bool {
709 self.x()
710 .is_some_and(|x| self.bucketable_candidates.iter().any(|c| c == x))
711 }
712
713 fn pool(&self, shelf: PickerFor) -> Vec<String> {
715 let mark = self.spec.mark;
716 let x = self.x();
717 match shelf {
718 PickerFor::X => match mark {
719 Mark::Line | Mark::Scatter => {
720 let mut out = self.temporal_candidates.clone();
721 for c in &self.numeric_candidates {
722 if !out.contains(c) {
723 out.push(c.clone());
724 }
725 }
726 out
727 }
728 Mark::Bar | Mark::Box => self.category_candidates.clone(),
729 Mark::Histogram | Mark::Kde | Mark::Heatmap => self.numeric_candidates.clone(),
730 },
731 PickerFor::Y
733 if self.spec.encoding.y.aggregate.takes_any_y() && self.takes_aggregate() =>
734 {
735 let mut all = self.numeric_candidates.clone();
736 for c in self
737 .temporal_candidates
738 .iter()
739 .chain(&self.category_candidates)
740 {
741 if !all.contains(c) {
742 all.push(c.clone());
743 }
744 }
745 all.retain(|c| Some(c) != x);
746 all
747 }
748 PickerFor::Y => self
750 .numeric_candidates
751 .iter()
752 .filter(|c| Some(*c) != x || mark == Mark::Box)
753 .cloned()
754 .collect(),
755 PickerFor::Color => self
756 .category_candidates
757 .iter()
758 .filter(|c| Some(*c) != x)
759 .cloned()
760 .collect(),
761 PickerFor::ColorValues => Vec::new(),
762 }
763 }
764
765 pub fn y_is_multi(&self) -> bool {
768 self.spec.mark.is_xy()
769 }
770
771 pub fn y_use(&self) -> ShelfUse {
773 match self.spec.mark {
774 Mark::Kde => ShelfUse::Dimmed("density"),
775 _ => ShelfUse::Used,
776 }
777 }
778
779 pub fn color_use(&self) -> ShelfUse {
781 Self::color_use_in(&self.spec)
782 }
783
784 pub fn color_use_in(spec: &ChartSpec) -> ShelfUse {
786 let y = &spec.encoding.y;
787 match spec.mark {
788 Mark::Box => ShelfUse::Dimmed("same as X"),
789 Mark::Heatmap => ShelfUse::Dimmed("density"),
790 Mark::Line | Mark::Scatter if y.field.len() > 1 => ShelfUse::Dimmed("one per Y column"),
791 Mark::Bar if y.aggregate == Aggregate::None => ShelfUse::Dimmed("needs an aggregate"),
792 _ => ShelfUse::Used,
793 }
794 }
795
796 pub fn colored_in(spec: &ChartSpec) -> bool {
798 spec.encoding.color.field.is_some() && Self::color_use_in(spec) == ShelfUse::Used
799 }
800
801 pub fn takes_aggregate(&self) -> bool {
803 matches!(self.spec.mark, Mark::Line | Mark::Scatter | Mark::Bar)
804 }
805
806 pub fn aggregates(&self) -> bool {
809 self.takes_aggregate() && self.aggregate() != Aggregate::None
810 }
811
812 pub fn colored(&self) -> bool {
814 Self::colored_in(&self.spec)
815 }
816
817 pub fn shows_other_in(spec: &ChartSpec) -> bool {
820 Self::colored_in(spec)
821 && spec
822 .encoding
823 .color
824 .other
825 .unwrap_or(spec.mark == Mark::Scatter)
826 }
827
828 pub fn shows_other(&self) -> bool {
829 Self::shows_other_in(&self.spec)
830 }
831
832 pub fn row_order(&self) -> Vec<ChartFocus> {
835 use ChartFocus::*;
836 let mark = self.spec.mark;
837 let mut rows = vec![Type, X];
838 match mark {
839 Mark::Line | Mark::Scatter if self.x_is_bucketable() => rows.push(TimeUnit),
840 Mark::Bar => rows.push(Order),
841 Mark::Histogram => rows.push(Bins),
842 _ => {}
843 }
844 if self.y_use() == ShelfUse::Used {
845 rows.push(Y);
846 }
847 if self.takes_aggregate() {
848 rows.push(Aggregate);
849 if self.aggregate() == self::Aggregate::Quantile {
850 rows.push(Quantile);
851 }
852 }
853 if self.color_use() == ShelfUse::Used {
854 rows.push(Color);
855 if self.color().is_some() {
856 rows.push(ColorValues);
857 }
858 }
859 match mark {
861 Mark::Line | Mark::Scatter => {
862 if self.aggregates() && self.aggregate().runs_cumulative() {
863 rows.push(Cumulative);
864 }
865 rows.extend([YStartsAtZero, LogScale, ShowLegend, Grid]);
866 }
867 Mark::Bar => rows.push(ShowLegend),
868 Mark::Histogram => rows.extend([Range, ShowLegend, Grid]),
869 Mark::Kde => rows.extend([Bandwidth, Range, ShowLegend, Grid]),
870 Mark::Box => rows.extend([Range, Grid]),
871 Mark::Heatmap => rows.push(Bins),
872 }
873 if !self.aggregates() && !self.view_sampled {
874 rows.push(LimitRows);
875 }
876 rows
877 }
878
879 pub fn has_grid(&self) -> bool {
882 self.row_order().contains(&ChartFocus::Grid)
883 }
884
885 pub fn set_mark(&mut self, mark: Mark) {
890 if mark == self.spec.mark {
891 return;
892 }
893 let old = self.spec.mark;
894 self.spec.mark = mark;
895 self.suggested = None;
896 self.close_picker();
897 if !mark.is_xy() {
898 self.plot_focus = false;
899 }
900 let encoding = &mut self.spec.encoding;
901 let numeric = |c: &String| self.numeric_candidates.contains(c);
904 let category = |c: &String| self.category_candidates.contains(c);
905 match mark {
906 Mark::Histogram | Mark::Kde => {
907 if !encoding.x.field.as_ref().is_some_and(numeric) {
908 encoding.x.field = encoding.y.field.first().cloned();
909 }
910 }
911 Mark::Box => {
912 if encoding.y.field.is_empty()
913 && let Some(x) = encoding.x.field.clone().filter(numeric)
914 {
915 encoding.y.field = vec![x];
916 encoding.x.field = None;
917 }
918 if !encoding.x.field.as_ref().is_some_and(category) {
919 encoding.x.field = None;
920 }
921 }
922 Mark::Bar => {
923 if !encoding.x.field.as_ref().is_some_and(category) {
924 encoding.x.field = encoding.color.field.take();
925 encoding.color.values.clear();
926 }
927 if encoding.y.field.is_empty() && encoding.y.aggregate == Aggregate::None {
928 encoding.y.aggregate = Aggregate::Count;
929 }
930 }
931 Mark::Heatmap => {
932 if !encoding.x.field.as_ref().is_some_and(numeric) {
933 encoding.x.field = None;
934 }
935 }
936 Mark::Line | Mark::Scatter => {
937 if matches!(old, Mark::Histogram | Mark::Kde | Mark::Box)
938 && encoding.y.field.is_empty()
939 && let Some(x) = encoding.x.field.take()
940 {
941 encoding.y.field = vec![x];
942 }
943 }
944 }
945 self.settle();
946 if !self.row_order().contains(&self.focus) {
947 self.focus = ChartFocus::Type;
948 }
949 }
950
951 fn settle(&mut self) {
953 let x_pool = self.pool(PickerFor::X);
954 let y_pool = self.pool(PickerFor::Y);
955 let color_pool = self.pool(PickerFor::Color);
956 let mark = self.spec.mark;
957 let x_temporal = self.x_is_bucketable();
958 let encoding = &mut self.spec.encoding;
959 if encoding
960 .x
961 .field
962 .as_ref()
963 .is_some_and(|x| !x_pool.contains(x))
964 {
965 encoding.x.field = None;
966 }
967 encoding.y.field.retain(|y| y_pool.contains(y));
968 if !mark.is_xy() {
969 encoding.y.field.truncate(1);
970 }
971 if !x_temporal || !mark.is_xy() || encoding.y.aggregate == Aggregate::None {
973 encoding.x.time_unit = TimeUnit::None;
974 }
975 if encoding
976 .color
977 .field
978 .as_ref()
979 .is_some_and(|c| !color_pool.contains(c))
980 {
981 encoding.color.field = None;
982 encoding.color.values.clear();
983 }
984 if !matches!(mark, Mark::Line | Mark::Scatter | Mark::Bar) {
985 encoding.y.aggregate = Aggregate::None;
986 }
987 if !encoding.y.aggregate.runs_cumulative() || !mark.is_xy() {
988 encoding.y.cumulative = Cumulative::Off;
989 }
990 }
991
992 pub fn step_mark(&mut self, delta: i8) {
993 let mark = crate::app::form::step_value(&Mark::ALL, self.spec.mark, delta);
994 self.set_mark(mark);
995 }
996
997 pub fn picker_for(&self, focus: ChartFocus) -> Option<PickerFor> {
1001 match focus {
1002 ChartFocus::X => Some(PickerFor::X),
1003 ChartFocus::Y if self.spec.mark != Mark::Histogram => Some(PickerFor::Y),
1004 ChartFocus::Color => Some(PickerFor::Color),
1005 ChartFocus::ColorValues => Some(PickerFor::ColorValues),
1006 _ => None,
1007 }
1008 }
1009
1010 pub fn picker_is_multi(&self, which: PickerFor) -> bool {
1012 match which {
1013 PickerFor::Y => self.y_is_multi(),
1014 PickerFor::ColorValues => true,
1015 PickerFor::X | PickerFor::Color => false,
1016 }
1017 }
1018
1019 fn offers_none(&self, which: PickerFor) -> bool {
1021 which == PickerFor::Color || (which == PickerFor::X && self.spec.mark == Mark::Box)
1022 }
1023
1024 fn picker_items(&self, which: PickerFor) -> (Vec<String>, Vec<String>) {
1026 if which == PickerFor::ColorValues {
1027 let null = crate::glyphs::get().null;
1028 let Some(counts) = &self.color_counts else {
1029 return (Vec::new(), Vec::new());
1030 };
1031 return counts
1032 .values
1033 .iter()
1034 .map(|(value, rows)| {
1035 (
1036 value.clone().unwrap_or_else(|| null.to_string()),
1037 crate::numfmt::group_chrome(*rows as usize),
1038 )
1039 })
1040 .unzip();
1041 }
1042 let mut items = Vec::new();
1043 if self.offers_none(which) {
1044 items.push(NONE_ITEM.to_string());
1045 }
1046 items.extend(self.pool(which));
1047 (items, Vec::new())
1048 }
1049
1050 fn color_value_at(&self, i: usize) -> Option<Option<String>> {
1052 self.color_counts
1053 .as_ref()?
1054 .values
1055 .get(i)
1056 .map(|(value, _)| value.clone())
1057 }
1058
1059 fn current_index(&self, which: PickerFor, items: &[String]) -> Option<usize> {
1061 let encoding = &self.spec.encoding;
1062 let current = match which {
1063 PickerFor::X => encoding.x.field.as_deref(),
1064 PickerFor::Y => encoding.y.field.first().map(String::as_str),
1065 PickerFor::Color => encoding.color.field.as_deref(),
1066 PickerFor::ColorValues => return Some(0),
1067 };
1068 match current {
1069 Some(current) => items.iter().position(|i| i == current),
1070 None if self.offers_none(which) => Some(0),
1071 None => None,
1072 }
1073 }
1074
1075 pub fn open_picker(&mut self) {
1078 let Some(which) = self.picker_for(self.focus) else {
1079 return;
1080 };
1081 if which == PickerFor::ColorValues && !self.has_color_counts() {
1082 return;
1083 }
1084 let (items, details) = self.picker_items(which);
1085 let mut state = PickerState::new(items.clone());
1086 if let Some(i) = self.current_index(which, &items) {
1087 state.select_original(i);
1088 }
1089 self.picker = Some(state);
1090 self.picker_for = Some(which);
1091 self.picker_details = details;
1092 }
1093
1094 pub fn has_color_counts(&self) -> bool {
1097 self.color_counts
1098 .as_ref()
1099 .is_some_and(|c| Some(&c.column) == self.color())
1100 }
1101
1102 pub fn step_picker_row(&mut self, delta: i8) {
1104 let Some(which) = self.picker_for(self.focus) else {
1105 return;
1106 };
1107 if self.picker_is_multi(which) {
1108 return;
1109 }
1110 let (items, _) = self.picker_items(which);
1111 if items.is_empty() {
1112 return;
1113 }
1114 let next = match self.current_index(which, &items) {
1115 Some(at) => crate::app::form::step_index(at, items.len(), delta),
1116 None if delta < 0 => items.len() - 1,
1117 None => 0,
1118 };
1119 self.choose(which, &items[next], next);
1120 }
1121
1122 fn picker_cursor(&self) -> Option<usize> {
1124 self.picker.as_ref()?.selected_original()
1125 }
1126
1127 pub fn picker_choose(&mut self) {
1131 let (Some(which), Some(i)) = (self.picker_for, self.picker_cursor()) else {
1132 self.close_picker();
1133 return;
1134 };
1135 let item = self.picker.as_ref().and_then(|p| p.items().get(i).cloned());
1136 self.close_picker();
1137 let Some(item) = item else {
1138 return;
1139 };
1140 match which {
1141 PickerFor::Y if self.y_is_multi() => {
1142 if self.spec.encoding.y.field.is_empty() {
1143 self.spec.encoding.y.field.push(item);
1144 }
1145 }
1146 PickerFor::ColorValues => {
1147 if self.spec.encoding.color.values.is_empty()
1148 && let Some(value) = self.color_value_at(i)
1149 {
1150 self.spec.encoding.color.values.push(value);
1151 }
1152 }
1153 _ => self.choose(which, &item, i),
1154 }
1155 self.settle();
1156 }
1157
1158 fn choose(&mut self, which: PickerFor, item: &str, i: usize) {
1160 let none = self.offers_none(which) && i == 0;
1161 let encoding = &mut self.spec.encoding;
1162 match which {
1163 PickerFor::X => {
1164 encoding.x.field = (!none).then(|| item.to_string());
1165 encoding.y.field.retain(|y| y != item);
1167 if encoding.color.field.as_deref() == Some(item) {
1168 encoding.color.field = None;
1169 encoding.color.values.clear();
1170 }
1171 encoding.x.time_unit = TimeUnit::None;
1172 }
1173 PickerFor::Y => encoding.y.field = vec![item.to_string()],
1174 PickerFor::Color => {
1175 let field = (!none).then(|| item.to_string());
1176 if field != encoding.color.field {
1177 encoding.color.values.clear();
1178 }
1179 encoding.color.field = field;
1180 }
1181 PickerFor::ColorValues => {}
1182 }
1183 self.settle();
1184 if !self.row_order().contains(&self.focus) {
1185 self.focus = ChartFocus::Type;
1186 }
1187 }
1188
1189 pub fn picker_toggle(&mut self) {
1192 let (Some(which), Some(i)) = (self.picker_for, self.picker_cursor()) else {
1193 return;
1194 };
1195 match which {
1196 PickerFor::Y if self.y_is_multi() => {
1197 let Some(item) = self.picker.as_ref().and_then(|p| p.items().get(i).cloned())
1198 else {
1199 return;
1200 };
1201 let most = Y_SERIES_MAX.min(self.series_max());
1202 let field = &mut self.spec.encoding.y.field;
1203 if let Some(pos) = field.iter().position(|c| *c == item) {
1204 field.remove(pos);
1205 } else if field.len() < most {
1206 field.push(item);
1207 }
1208 }
1209 PickerFor::ColorValues => {
1210 let Some(value) = self.color_value_at(i) else {
1211 return;
1212 };
1213 let most = self.series_max();
1214 let values = &mut self.spec.encoding.color.values;
1215 if let Some(pos) = values.iter().position(|v| *v == value) {
1216 values.remove(pos);
1217 } else if values.len() < most {
1218 values.push(value);
1219 }
1220 }
1221 _ => {}
1222 }
1223 }
1224
1225 pub fn is_marked(&self, i: usize) -> bool {
1227 match self.picker_for {
1228 Some(PickerFor::Y) => self
1229 .picker
1230 .as_ref()
1231 .and_then(|p| p.items().get(i))
1232 .is_some_and(|item| self.y().contains(item)),
1233 Some(PickerFor::ColorValues) => self
1234 .color_value_at(i)
1235 .is_some_and(|v| self.spec.encoding.color.values.contains(&v)),
1236 _ => false,
1237 }
1238 }
1239
1240 pub fn picker_multi(&self) -> bool {
1242 self.picker_for.is_some_and(|w| self.picker_is_multi(w))
1243 }
1244
1245 pub fn effective_spec(&self) -> ChartSpec {
1251 let mut spec = self.spec.clone();
1252 let (Some(which), Some(i)) = (self.picker_for, self.picker_cursor()) else {
1253 return spec;
1254 };
1255 let Some(item) = self.picker.as_ref().and_then(|p| p.items().get(i).cloned()) else {
1256 return spec;
1257 };
1258 match which {
1259 PickerFor::Y if self.y_is_multi() => {
1260 if !spec.encoding.y.field.contains(&item) {
1261 spec.encoding.y.field.push(item);
1262 }
1263 }
1264 PickerFor::Y => spec.encoding.y.field = vec![item],
1265 PickerFor::X if !spec.mark.is_xy() => {
1266 spec.encoding.x.field = (!(self.offers_none(which) && i == 0)).then_some(item);
1267 }
1268 _ => {}
1269 }
1270 spec
1271 }
1272
1273 pub fn step(&mut self, focus: ChartFocus, delta: i8) {
1277 match focus {
1278 ChartFocus::Type => self.step_mark(delta),
1279 ChartFocus::TimeUnit => {
1280 let encoding = &mut self.spec.encoding;
1281 encoding.x.time_unit =
1282 crate::app::form::step_value(&TimeUnit::ALL, encoding.x.time_unit, delta);
1283 if encoding.x.time_unit != TimeUnit::None && encoding.y.aggregate == Aggregate::None
1285 {
1286 encoding.y.aggregate = Aggregate::Mean;
1287 }
1288 }
1289 ChartFocus::Aggregate => {
1290 let was = self.aggregate();
1291 let bucketable = self.x_is_bucketable() && self.spec.mark.is_xy();
1292 let encoding = &mut self.spec.encoding;
1293 encoding.y.aggregate =
1294 crate::app::form::step_value(&Aggregate::ALL, encoding.y.aggregate, delta);
1295 if was == Aggregate::None
1298 && bucketable
1299 && encoding.y.aggregate != Aggregate::None
1300 && encoding.x.time_unit == TimeUnit::None
1301 {
1302 encoding.x.time_unit = TimeUnit::Day;
1303 }
1304 self.settle();
1305 }
1306 ChartFocus::Cumulative => {
1307 let y = &mut self.spec.encoding.y;
1308 y.cumulative = crate::app::form::step_value(&Cumulative::ALL, y.cumulative, delta);
1309 }
1310 ChartFocus::Quantile => {
1311 let y = &mut self.spec.encoding.y;
1312 y.percentile = Some(crate::app::form::step_value(
1313 &QUANTILES,
1314 y.quantile(),
1315 delta,
1316 ));
1317 }
1318 ChartFocus::Y if self.spec.mark == Mark::Histogram => self.share = !self.share,
1319 ChartFocus::Order => {
1320 self.bar_order =
1321 crate::app::form::step_value(&BarOrder::ALL, self.bar_order, delta);
1322 }
1323 ChartFocus::Range => {
1324 self.value_range =
1325 crate::app::form::step_value(&ValueRange::ALL, self.value_range, delta);
1326 }
1327 ChartFocus::Bins => self.adjust_bins(delta.into()),
1328 ChartFocus::Bandwidth => {
1329 self.adjust_kde_bandwidth_factor(f64::from(delta) * KDE_BANDWIDTH_STEP)
1330 }
1331 ChartFocus::LimitRows => self.toggle_rows(),
1332 ChartFocus::YStartsAtZero => self.y_starts_at_zero = !self.y_starts_at_zero,
1333 ChartFocus::LogScale => self.log_scale = !self.log_scale,
1334 ChartFocus::ShowLegend => self.show_legend = !self.show_legend,
1335 ChartFocus::Grid => self.grid = !self.grid,
1336 ChartFocus::ColorValues => {
1338 self.spec.encoding.color.other = Some(!self.shows_other());
1339 }
1340 focus => {
1341 if self.picker_for(focus).is_some() {
1342 self.step_picker_row(delta);
1343 }
1344 }
1345 }
1346 if !self.row_order().contains(&self.focus) {
1347 self.focus = ChartFocus::Type;
1348 self.leave_rows();
1349 }
1350 }
1351
1352 pub fn is_toggle_row(&self, focus: ChartFocus) -> bool {
1353 matches!(
1354 focus,
1355 ChartFocus::YStartsAtZero
1356 | ChartFocus::LogScale
1357 | ChartFocus::ShowLegend
1358 | ChartFocus::Grid
1359 )
1360 }
1361
1362 pub fn toggle_grid(&mut self) {
1363 self.grid = !self.grid;
1364 }
1365
1366 fn adjust_bins(&mut self, delta: i32) {
1367 if self.spec.mark == Mark::Heatmap {
1368 self.heatmap_bins = (self.heatmap_bins as i32 + delta)
1369 .clamp(HEATMAP_MIN_BINS as i32, HEATMAP_MAX_BINS as i32)
1370 as usize;
1371 } else {
1372 self.hist_bins = (self.hist_bins as i32 + delta)
1373 .clamp(HISTOGRAM_MIN_BINS as i32, HISTOGRAM_MAX_BINS as i32)
1374 as usize;
1375 }
1376 }
1377
1378 pub fn adjust_kde_bandwidth_factor(&mut self, delta: f64) {
1379 let next = (self.kde_bandwidth_factor + delta).clamp(KDE_BANDWIDTH_MIN, KDE_BANDWIDTH_MAX);
1380 self.kde_bandwidth_factor = (next * 10.0).round() / 10.0;
1381 }
1382
1383 pub fn adjust_number_row(&mut self, delta: i32) {
1385 match self.focus {
1386 ChartFocus::Bins => self.adjust_bins(delta),
1387 ChartFocus::Bandwidth => {
1388 self.adjust_kde_bandwidth_factor(delta as f64 * KDE_BANDWIDTH_STEP)
1389 }
1390 _ => {}
1391 }
1392 }
1393
1394 pub fn rows_shown(&self) -> RowsDraft {
1398 self.rows_draft.clone().unwrap_or(RowsDraft {
1399 every: self.row_limit.is_none(),
1400 ..RowsDraft::default()
1401 })
1402 }
1403
1404 pub fn rows_pending(&self) -> bool {
1406 self.rows_draft
1407 .as_ref()
1408 .is_some_and(|draft| draft.typed.is_some() || draft.every != self.row_limit.is_none())
1409 }
1410
1411 pub fn toggle_rows(&mut self) {
1414 if !self.take_typed_size() {
1415 return;
1416 }
1417 let mut draft = self.rows_shown();
1418 draft.every = !draft.every;
1419 self.rows_draft = Some(draft);
1420 }
1421
1422 pub fn type_rows(&mut self, c: char) {
1424 let mut draft = self.rows_shown();
1425 draft.every = false;
1426 draft.error = None;
1427 draft.typed.get_or_insert_with(String::new).push(c);
1428 self.rows_draft = Some(draft);
1429 }
1430
1431 pub fn typing_rows(&self) -> bool {
1433 self.rows_draft.as_ref().is_some_and(|d| d.typed.is_some())
1434 }
1435
1436 pub fn backspace_rows(&mut self) {
1438 if let Some(draft) = self.rows_draft.as_mut()
1439 && let Some(typed) = draft.typed.as_mut()
1440 {
1441 typed.pop();
1442 draft.error = None;
1443 if typed.is_empty() {
1444 draft.typed = None;
1445 }
1446 }
1447 }
1448
1449 pub fn discard_rows(&mut self) {
1451 self.rows_draft = None;
1452 }
1453
1454 fn take_typed_size(&mut self) -> bool {
1457 let Some(draft) = self.rows_draft.as_mut() else {
1458 return true;
1459 };
1460 let Some(typed) = draft.typed.take() else {
1461 return true;
1462 };
1463 match crate::analysis::sampling::parse_size(&typed) {
1464 Ok(rows) => {
1465 let rows = rows.min(CHART_ROW_LIMIT_MAX);
1466 if self.view_rows.is_some_and(|total| rows >= total) {
1468 draft.every = true;
1469 } else {
1470 draft.every = false;
1471 self.sample_rows = rows;
1472 }
1473 true
1474 }
1475 Err(e) => {
1476 draft.error = Some(e.short());
1477 draft.typed = Some(typed);
1478 false
1479 }
1480 }
1481 }
1482
1483 pub fn commit_rows(&mut self) -> bool {
1486 if !self.take_typed_size() {
1487 return false;
1488 }
1489 if let Some(draft) = self.rows_draft.take() {
1490 self.row_limit = (!draft.every).then_some(self.sample_rows);
1491 }
1492 true
1493 }
1494
1495 pub fn leave_rows(&mut self) {
1498 if !self.commit_rows() {
1499 self.rows_draft = None;
1500 }
1501 }
1502
1503 pub fn is_complete(spec: &ChartSpec) -> bool {
1505 let encoding = &spec.encoding;
1506 let x = encoding.x.field.is_some();
1507 let y = !encoding.y.field.is_empty();
1508 match spec.mark {
1509 Mark::Line | Mark::Scatter => x && (y || encoding.y.aggregate == Aggregate::Count),
1510 Mark::Bar => x && (y || encoding.y.aggregate == Aggregate::Count),
1511 Mark::Histogram | Mark::Kde => x,
1512 Mark::Box => y,
1513 Mark::Heatmap => x && y,
1514 }
1515 }
1516
1517 pub fn can_export(&self) -> bool {
1518 Self::is_complete(&self.effective_spec())
1519 }
1520
1521 pub fn how(&self) -> String {
1525 let spec = self.effective_spec();
1526 let encoding = &spec.encoding;
1527 let x = encoding.x.field.clone().unwrap_or_default();
1528 let mut parts: Vec<String> = Vec::new();
1529 match spec.mark {
1530 Mark::Histogram => parts.push(if self.share {
1531 "share per bin".to_string()
1532 } else {
1533 "count per bin".to_string()
1534 }),
1535 Mark::Kde => {}
1537 Mark::Box if !x.is_empty() => parts.push(format!("one box per {x}")),
1538 Mark::Box => {}
1539 Mark::Heatmap => parts.push("rows per cell".to_string()),
1540 Mark::Line | Mark::Scatter | Mark::Bar => {
1541 let aggregate = encoding.y.aggregate;
1542 let mut how = String::new();
1543 if aggregate != Aggregate::None && encoding.y.cumulative == Cumulative::Off {
1545 how.push_str(&encoding.y.aggregate_name());
1546 how.push(' ');
1547 }
1548 let unit = encoding.x.time_unit;
1549 if unit != TimeUnit::None {
1550 how.push_str(&format!("by {}", unit.label()));
1551 } else if aggregate != Aggregate::None {
1552 how.push_str(&format!("by {x}"));
1553 }
1554 if !how.trim().is_empty() {
1555 parts.push(how.trim().to_string());
1556 }
1557 match encoding.y.cumulative {
1558 Cumulative::Off => {}
1559 Cumulative::Sum => parts.push("running sum".to_string()),
1560 Cumulative::Compound => parts.push("compounded".to_string()),
1561 }
1562 }
1563 }
1564 if self.colored() {
1565 parts.push(format!("colored by {}", self.color().unwrap()));
1566 }
1567 parts.join(", ")
1568 }
1569}
1570
1571impl crate::app::form::Form for ChartModal {
1572 type Field = ChartFocus;
1573
1574 fn shown_picker(&mut self) -> Option<(&mut crate::widgets::ui::PickerState, bool)> {
1575 let multi = self.picker_multi();
1576 self.picker.as_mut().map(|p| (p, multi))
1577 }
1578
1579 fn dismiss_picker(&mut self) {
1580 ChartModal::close_picker(self);
1581 }
1582
1583 fn pick(&mut self, toggle: bool) {
1584 if toggle {
1585 self.picker_toggle();
1586 } else {
1587 self.picker_choose();
1588 }
1589 }
1590
1591 fn fields(&self) -> Vec<(ChartFocus, crate::app::form::FieldKind)> {
1592 use crate::app::form::FieldKind;
1593 self.row_order()
1594 .into_iter()
1595 .map(|row| {
1596 let kind = match self.picker_for(row) {
1597 Some(which) => FieldKind::Picker {
1598 multi: self.picker_is_multi(which),
1599 },
1600 None if self.is_toggle_row(row) => FieldKind::Checkbox,
1601 None => FieldKind::Choice,
1603 };
1604 (row, kind)
1605 })
1606 .collect()
1607 }
1608
1609 fn focused(&self) -> ChartFocus {
1610 self.focus
1611 }
1612
1613 fn set_focused(&mut self, field: ChartFocus) {
1614 if field != ChartFocus::LimitRows {
1615 self.leave_rows();
1616 }
1617 self.focus = field;
1618 }
1619}
1620
1621#[cfg(test)]
1622mod tests {
1623 use super::*;
1624 use crate::app::form::Form;
1625
1626 fn s(v: &[&str]) -> Vec<String> {
1627 v.iter().map(|s| s.to_string()).collect()
1628 }
1629
1630 struct Cols {
1631 numeric: Vec<String>,
1632 datetime: Vec<String>,
1633 category: Vec<String>,
1634 }
1635
1636 fn cols() -> Cols {
1637 Cols {
1638 numeric: s(&["delay", "distance", "year"]),
1639 datetime: s(&["date"]),
1640 category: s(&["carrier", "origin", "year"]),
1641 }
1642 }
1643
1644 fn open_on(cursor: Option<(&str, &DataType)>) -> ChartModal {
1645 let c = cols();
1646 let mut modal = ChartModal::new();
1647 modal.open(
1648 ChartColumns {
1649 numeric: &c.numeric,
1650 datetime: &c.datetime,
1651 bucketable: &c.datetime,
1652 category: &c.category,
1653 },
1654 cursor,
1655 Some(10_000),
1656 false,
1657 1,
1658 );
1659 modal
1660 }
1661
1662 #[test]
1663 fn an_axis_title_names_the_unit() {
1664 let mut modal = ChartModal::default();
1665 assert_eq!(modal.axis_title("cht1"), "cht1");
1666 modal.units = vec![("cht1".to_string(), "deg F".to_string())];
1667 assert_eq!(modal.axis_title("cht1"), "cht1 (deg F)");
1668 }
1669
1670 #[test]
1672 fn quick_chart_follows_the_cursor_column_type() {
1673 let modal = open_on(Some(("delay", &DataType::Float64)));
1674 assert_eq!(modal.mark(), Mark::Histogram);
1675 assert_eq!(modal.x().map(String::as_str), Some("delay"));
1676 assert_eq!(modal.suggested.as_deref(), Some("f64"));
1677
1678 let modal = open_on(Some(("carrier", &DataType::String)));
1679 assert_eq!(modal.mark(), Mark::Bar);
1680 assert_eq!(modal.aggregate(), Aggregate::Count);
1681 assert!(ChartModal::is_complete(&modal.spec), "counts need no Y");
1682
1683 let modal = open_on(Some(("date", &DataType::Date)));
1684 assert_eq!(modal.mark(), Mark::Line);
1685 assert_eq!(modal.x().map(String::as_str), Some("date"));
1686 assert_eq!(modal.y(), ["delay"], "the first numeric column");
1687
1688 let modal = open_on(Some(("year", &DataType::Int64)));
1689 assert_eq!(modal.mark(), Mark::Histogram, "an integer is a measure");
1690
1691 let modal = open_on(None);
1692 assert_eq!(modal.mark(), Mark::Line);
1693 assert!(modal.x().is_none() && modal.suggested.is_none());
1694 }
1695
1696 #[test]
1697 fn changing_the_type_clears_the_suggestion() {
1698 let mut modal = open_on(Some(("delay", &DataType::Float64)));
1699 modal.step(ChartFocus::Type, 1);
1700 assert_eq!(modal.mark(), Mark::Box);
1701 assert!(modal.suggested.is_none());
1702 assert_eq!(modal.y(), ["delay"], "the histogram's value is the box's");
1703 assert!(modal.x().is_none(), "a box's X is a category or none");
1704 }
1705
1706 #[test]
1709 fn shelves_dim_by_type() {
1710 let mut modal = open_on(None);
1711 let rows = |m: &ChartModal| m.row_order();
1712 assert!(rows(&modal).contains(&ChartFocus::Color));
1713 modal.set_mark(Mark::Kde);
1714 assert_eq!(modal.y_use(), ShelfUse::Dimmed("density"));
1715 assert!(!rows(&modal).contains(&ChartFocus::Y));
1716 modal.set_mark(Mark::Box);
1717 assert_eq!(modal.color_use(), ShelfUse::Dimmed("same as X"));
1718 assert!(!rows(&modal).contains(&ChartFocus::Color));
1719 modal.set_mark(Mark::Heatmap);
1720 assert_eq!(modal.color_use(), ShelfUse::Dimmed("density"));
1721 modal.set_mark(Mark::Line);
1722 modal.spec.encoding.y.field = s(&["delay", "distance"]);
1723 assert_eq!(modal.color_use(), ShelfUse::Dimmed("one per Y column"));
1724 modal.set_mark(Mark::Bar);
1725 modal.spec.encoding.y.aggregate = Aggregate::None;
1726 assert_eq!(modal.color_use(), ShelfUse::Dimmed("needs an aggregate"));
1727 }
1728
1729 #[test]
1733 fn rows_follow_the_type() {
1734 use ChartFocus::*;
1735 let mut modal = open_on(Some(("date", &DataType::Date)));
1736 assert_eq!(
1737 modal.fields().iter().map(|(f, _)| *f).collect::<Vec<_>>(),
1738 [
1739 Type,
1740 X,
1741 TimeUnit,
1742 Y,
1743 Aggregate,
1744 Color,
1745 YStartsAtZero,
1746 LogScale,
1747 ShowLegend,
1748 Grid,
1749 LimitRows
1750 ]
1751 );
1752 modal.step(TimeUnit, 1);
1753 assert_eq!(modal.spec.encoding.x.time_unit, super::TimeUnit::Day);
1754 assert_eq!(
1755 modal.aggregate(),
1756 super::Aggregate::Mean,
1757 "a bucket needs one"
1758 );
1759 let rows = modal.row_order();
1760 assert!(rows.contains(&Cumulative) && !rows.contains(&LimitRows));
1761
1762 modal.set_mark(Mark::Bar);
1763 assert_eq!(modal.spec.encoding.x.time_unit, super::TimeUnit::None);
1764 assert!(modal.row_order().contains(&Order));
1765 modal.set_mark(Mark::Histogram);
1766 assert!(modal.row_order().contains(&Bins));
1767 }
1768
1769 fn step_to(modal: &mut ChartModal, to: Aggregate, delta: i8) {
1771 for _ in 0..Aggregate::ALL.len() {
1772 if modal.aggregate() == to {
1773 return;
1774 }
1775 modal.step(ChartFocus::Aggregate, delta);
1776 }
1777 assert_eq!(modal.aggregate(), to);
1778 }
1779
1780 #[test]
1781 fn the_aggregate_steps_through_every_one() {
1782 let mut modal = open_on(Some(("date", &DataType::Date)));
1783 let labels: Vec<&str> = (0..Aggregate::ALL.len())
1784 .map(|_| {
1785 modal.step(ChartFocus::Aggregate, 1);
1786 modal.aggregate().label()
1787 })
1788 .collect();
1789 assert_eq!(
1790 labels,
1791 [
1792 "count", "distinct", "sum", "mean", "median", "stdev", "quantile", "min", "max",
1793 "first", "last", "none"
1794 ]
1795 );
1796 modal.step(ChartFocus::Aggregate, -1);
1797 assert_eq!(modal.aggregate(), Aggregate::Last);
1798 }
1799
1800 #[test]
1801 fn cumulative_goes_with_the_aggregate() {
1802 let mut modal = open_on(Some(("date", &DataType::Date)));
1803 modal.spec.encoding.y.aggregate = Aggregate::Sum;
1804 modal.step(ChartFocus::Cumulative, 1);
1805 assert_eq!(modal.spec.encoding.y.cumulative, Cumulative::Sum);
1806 modal.step(ChartFocus::Cumulative, 1);
1807 assert_eq!(modal.spec.encoding.y.cumulative, Cumulative::Compound);
1808 step_to(&mut modal, Aggregate::None, -1);
1809 assert_eq!(modal.spec.encoding.y.cumulative, Cumulative::Off);
1810 }
1811
1812 #[test]
1813 fn color_picks_a_category_then_values_by_count() {
1814 let mut modal = open_on(Some(("delay", &DataType::Float64)));
1815 modal.focus = ChartFocus::Color;
1816 modal.open_picker();
1817 let items = modal.picker.as_ref().unwrap().items().to_vec();
1818 assert_eq!(items, ["none", "carrier", "origin", "year"]);
1819 modal.picker.as_mut().unwrap().select_original(1);
1820 modal.picker_choose();
1821 assert_eq!(modal.color().map(String::as_str), Some("carrier"));
1822 assert!(modal.row_order().contains(&ChartFocus::ColorValues));
1823
1824 modal.focus = ChartFocus::ColorValues;
1826 modal.open_picker();
1827 assert!(modal.picker.is_none());
1828 modal.color_counts = Some(ColorCounts {
1829 column: "carrier".to_string(),
1830 values: (0..COLOR_MAX + 2)
1831 .map(|i| (Some(format!("C{i}")), 100 - i as u64))
1832 .chain([(None, 1)])
1833 .collect(),
1834 });
1835 modal.open_picker();
1836 assert_eq!(modal.picker_details[0], "100");
1837 for _ in 0..COLOR_MAX + 2 {
1838 modal.picker_toggle();
1839 modal.picker.as_mut().unwrap().move_down();
1840 }
1841 assert_eq!(modal.spec.encoding.color.values.len(), COLOR_MAX, "capped");
1842 assert!(modal.is_marked(0));
1843 modal.picker.as_mut().unwrap().select_original(0);
1844 modal.picker_toggle();
1845 assert!(!modal.is_marked(0));
1846 modal.picker_choose();
1847 assert_eq!(modal.spec.encoding.color.values.len(), COLOR_MAX - 1);
1848
1849 modal.focus = ChartFocus::Color;
1851 modal.step(ChartFocus::Color, 1);
1852 assert_eq!(modal.color().map(String::as_str), Some("origin"));
1853 assert!(modal.spec.encoding.color.values.is_empty());
1854 modal.step(ChartFocus::Color, -1);
1855 modal.step(ChartFocus::Color, -1);
1856 assert!(modal.color().is_none(), "none is the first choice");
1857 }
1858
1859 #[test]
1860 fn the_spec_saves_its_time_unit_and_aggregate() {
1861 let mut modal = open_on(Some(("date", &DataType::Date)));
1862 modal.step(ChartFocus::TimeUnit, 3);
1863 let saved = serde_json::to_value(&modal.spec).unwrap();
1864 assert_eq!(saved["encoding"]["x"]["timeUnit"], "month");
1865 assert_eq!(saved["encoding"]["y"]["aggregate"], "mean");
1866 }
1867
1868 #[test]
1869 fn the_title_says_how_the_rows_were_made() {
1870 let mut modal = open_on(Some(("date", &DataType::Date)));
1871 modal.step(ChartFocus::TimeUnit, 3);
1872 modal.spec.encoding.y.cumulative = Cumulative::Sum;
1873 modal.spec.encoding.color.field = Some("carrier".to_string());
1874 assert_eq!(modal.how(), "by month, running sum, colored by carrier");
1875 }
1876
1877 #[test]
1881 fn distinct_takes_any_y_and_no_cumulative() {
1882 let mut modal = open_on(Some(("date", &DataType::Date)));
1883 step_to(&mut modal, Aggregate::Distinct, 1);
1884 modal.focus = ChartFocus::Y;
1885 modal.open_picker();
1886 let items = modal.picker.as_ref().unwrap().items().to_vec();
1887 assert!(items.contains(&"carrier".to_string()), "{items:?}");
1888 modal.close_picker();
1889 modal.spec.encoding.y.field = vec!["carrier".to_string()];
1890 modal.step(ChartFocus::TimeUnit, 2);
1891 assert_eq!(modal.how(), "distinct by month");
1892 assert!(!modal.row_order().contains(&ChartFocus::Cumulative));
1893 modal.spec.encoding.y.cumulative = Cumulative::Sum;
1894 modal.step(ChartFocus::Aggregate, 0);
1895 assert_eq!(modal.spec.encoding.y.cumulative, Cumulative::Off);
1896 modal.step(ChartFocus::Aggregate, 1);
1898 assert_eq!(modal.aggregate(), Aggregate::Sum);
1899 assert!(modal.spec.encoding.y.field.is_empty());
1900 }
1901
1902 #[test]
1905 fn a_quantile_steps_its_percentile() {
1906 let mut modal = open_on(Some(("date", &DataType::Date)));
1907 step_to(&mut modal, Aggregate::Quantile, 1);
1908 modal.step(ChartFocus::TimeUnit, 2);
1909 assert_eq!(modal.spec.encoding.x.time_unit, TimeUnit::Month);
1910 let rows = modal.row_order();
1911 let at = rows
1912 .iter()
1913 .position(|r| *r == ChartFocus::Aggregate)
1914 .unwrap();
1915 assert_eq!(rows[at + 1], ChartFocus::Quantile);
1916 assert_eq!(modal.how(), "p90 by month");
1917 modal.step(ChartFocus::Quantile, 1);
1918 assert_eq!(modal.spec.encoding.y.quantile(), 95);
1919 assert_eq!(modal.how(), "p95 by month");
1920 modal.step(ChartFocus::Quantile, 1);
1921 modal.step(ChartFocus::Quantile, 1);
1922 assert_eq!(modal.spec.encoding.y.quantile(), 1, "wraps");
1923 for aggregate in [
1924 Aggregate::Stdev,
1925 Aggregate::Quantile,
1926 Aggregate::First,
1927 Aggregate::Last,
1928 ] {
1929 modal.spec.encoding.y.aggregate = aggregate;
1930 assert!(!modal.row_order().contains(&ChartFocus::Cumulative));
1931 }
1932 modal.spec.encoding.y.aggregate = Aggregate::Last;
1933 assert!(!modal.row_order().contains(&ChartFocus::Quantile));
1934 assert_eq!(modal.how(), "last by month");
1935 }
1936
1937 #[test]
1940 fn the_how_names_no_y_column() {
1941 let mut modal = open_on(Some(("date", &DataType::Date)));
1942 modal.step(ChartFocus::TimeUnit, 3);
1943 assert_eq!(modal.how(), "mean by month");
1944 modal.set_mark(Mark::Scatter);
1945 modal.spec.encoding.x.time_unit = TimeUnit::None;
1946 modal.spec.encoding.y.aggregate = Aggregate::None;
1947 assert_eq!(modal.how(), "");
1948 modal.spec.encoding.color.field = Some("carrier".to_string());
1949 assert_eq!(modal.how(), "colored by carrier");
1950 modal.spec.encoding.color.field = None;
1951 modal.spec.encoding.y.aggregate = Aggregate::Mean;
1952 assert_eq!(modal.how(), "mean by date");
1953 modal.set_mark(Mark::Histogram);
1954 assert_eq!(modal.how(), "count per bin");
1955 modal.set_mark(Mark::Kde);
1956 assert_eq!(modal.how(), "");
1957 modal.set_mark(Mark::Heatmap);
1958 assert_eq!(modal.how(), "rows per cell");
1959 for mark in Mark::ALL {
1960 modal.set_mark(mark);
1961 assert!(!modal.how().contains("delay"), "{mark:?}: {}", modal.how());
1962 }
1963 }
1964
1965 #[test]
1968 fn reopening_from_the_same_column_keeps_the_chart() {
1969 let mut modal = open_on(Some(("delay", &DataType::Float64)));
1970 modal.set_mark(Mark::Kde);
1971 modal.toggle_grid();
1972 modal.close();
1973 let c = cols();
1974 let columns = ChartColumns {
1975 numeric: &c.numeric,
1976 datetime: &c.datetime,
1977 bucketable: &c.datetime,
1978 category: &c.category,
1979 };
1980 modal.open(columns, Some(("delay", &DataType::Float64)), None, false, 1);
1981 assert_eq!(modal.mark(), Mark::Kde);
1982 assert!(modal.grid);
1983 modal.open(
1984 columns,
1985 Some(("carrier", &DataType::String)),
1986 None,
1987 false,
1988 1,
1989 );
1990 assert_eq!(modal.mark(), Mark::Bar);
1991 assert!(!modal.grid);
1992 }
1993
1994 #[test]
1995 fn the_y_picker_leaves_out_x_and_caps_its_series() {
1996 let mut modal = open_on(Some(("date", &DataType::Date)));
1997 modal.spec.encoding.x.field = Some("delay".to_string());
1998 modal.spec.encoding.y.field.clear();
1999 modal.focus = ChartFocus::Y;
2000 modal.open_picker();
2001 assert_eq!(modal.picker.as_ref().unwrap().items(), ["distance", "year"]);
2002 assert!(modal.picker_multi());
2003 modal.picker_choose();
2004 assert_eq!(
2005 modal.y(),
2006 ["distance"],
2007 "Enter on a fresh list takes the cursor"
2008 );
2009 }
2010
2011 #[test]
2012 fn number_rows_route_by_the_type() {
2013 let mut modal = open_on(Some(("delay", &DataType::Float64)));
2014 modal.focus = ChartFocus::Bins;
2015 modal.adjust_number_row(1);
2016 assert_eq!(modal.hist_bins, HISTOGRAM_DEFAULT_BINS + 1);
2017 modal.set_mark(Mark::Heatmap);
2018 modal.focus = ChartFocus::Bins;
2019 modal.adjust_number_row(-1);
2020 assert_eq!(modal.heatmap_bins, HEATMAP_DEFAULT_BINS - 1);
2021 }
2022
2023 #[test]
2026 fn rows_change_is_read_on_enter() {
2027 let mut modal = open_on(Some(("delay", &DataType::Float64)));
2028 modal.view_rows = Some(36_800_000);
2029 modal.focus = ChartFocus::LimitRows;
2030 assert_eq!(modal.row_limit, Some(10_000));
2031 modal.step(ChartFocus::LimitRows, 1);
2032 assert!(modal.rows_shown().every && modal.rows_pending());
2033 assert_eq!(modal.row_limit, Some(10_000), "pending until Enter");
2034 modal.step(ChartFocus::LimitRows, -1);
2035 assert!(!modal.rows_pending(), "back where it was");
2036 modal.step(ChartFocus::LimitRows, 1);
2037 assert!(modal.commit_rows());
2038 assert_eq!(modal.row_limit, None);
2039
2040 for c in "250kx".chars() {
2042 modal.type_rows(c);
2043 }
2044 modal.backspace_rows();
2045 assert_eq!(modal.rows_shown().typed.as_deref(), Some("250k"));
2046 assert!(!modal.rows_shown().every);
2047 modal.discard_rows();
2048 assert_eq!(modal.row_limit, None);
2049 assert!(modal.rows_shown().every);
2050
2051 for c in "250k".chars() {
2052 modal.type_rows(c);
2053 }
2054 assert!(modal.commit_rows());
2055 assert_eq!(modal.row_limit, Some(250_000));
2056 assert_eq!(modal.rows_draft, None);
2057
2058 modal.type_rows('0');
2060 assert!(!modal.commit_rows());
2061 assert!(modal.rows_shown().error.is_some());
2062 assert_eq!(modal.row_limit, Some(250_000));
2063 modal.backspace_rows();
2064 modal.type_rows('2');
2065 modal.type_rows('m');
2066 crate::app::form::Form::focus(&mut modal, ChartFocus::Type);
2068 assert_eq!(modal.row_limit, Some(2_000_000));
2069
2070 modal.focus = ChartFocus::LimitRows;
2072 for c in "40m".chars() {
2073 modal.type_rows(c);
2074 }
2075 assert!(modal.commit_rows());
2076 assert_eq!(modal.row_limit, None);
2077 modal.step(ChartFocus::LimitRows, 1);
2078 assert!(modal.commit_rows());
2079 assert_eq!(modal.row_limit, Some(2_000_000));
2080 }
2081
2082 #[test]
2085 fn a_bucket_needs_an_aggregate_and_a_date() {
2086 let mut modal = open_on(Some(("date", &DataType::Date)));
2087 modal.step(ChartFocus::TimeUnit, 3);
2088 assert_eq!(modal.spec.encoding.x.time_unit, TimeUnit::Month);
2089 modal.spec.encoding.y.aggregate = Aggregate::Sum;
2090 step_to(&mut modal, Aggregate::None, -1);
2091 assert_eq!(modal.spec.encoding.x.time_unit, TimeUnit::None);
2092
2093 let numeric = s(&["delay"]);
2094 let datetime = s(&["date", "clock"]);
2095 let mut modal = ChartModal::new();
2096 modal.open(
2097 ChartColumns {
2098 numeric: &numeric,
2099 datetime: &datetime,
2100 bucketable: &datetime[..1],
2101 category: &[],
2102 },
2103 Some(("clock", &DataType::Time)),
2104 None,
2105 false,
2106 1,
2107 );
2108 assert_eq!(modal.mark(), Mark::Line);
2109 assert!(!modal.row_order().contains(&ChartFocus::TimeUnit));
2110 }
2111
2112 #[test]
2115 fn an_aggregate_on_a_date_starts_by_the_day() {
2116 let mut modal = open_on(Some(("date", &DataType::Date)));
2117 assert_eq!(modal.spec.encoding.x.time_unit, TimeUnit::None);
2118 modal.step(ChartFocus::Aggregate, 1);
2119 assert_eq!(modal.spec.encoding.x.time_unit, TimeUnit::Day);
2120 step_to(&mut modal, Aggregate::Mean, 1);
2121 assert_eq!(modal.spec.encoding.x.time_unit, TimeUnit::Day);
2122 modal.step(ChartFocus::TimeUnit, -1);
2123 assert_eq!(
2124 modal.spec.encoding.x.time_unit,
2125 TimeUnit::None,
2126 "still the user's call"
2127 );
2128 }
2129
2130 #[test]
2133 fn a_box_from_an_integer_histogram_has_no_category() {
2134 let mut modal = open_on(Some(("year", &DataType::Int64)));
2135 modal.set_mark(Mark::Box);
2136 assert_eq!(modal.y(), ["year"]);
2137 assert!(modal.x().is_none());
2138 }
2139
2140 #[test]
2143 fn color_use_reads_the_spec_charted() {
2144 let mut modal = open_on(Some(("date", &DataType::Date)));
2145 modal.spec.encoding.color.field = Some("carrier".to_string());
2146 assert!(modal.colored());
2147 let mut spec = modal.spec.clone();
2148 spec.encoding.y.field.push("distance".to_string());
2149 assert!(!ChartModal::colored_in(&spec));
2150 }
2151}