flat/dagchart/
model.rs

1use crate::abbreviate::find_abbreviations;
2use crate::aggregate::{aggregate_apply, minimal_precision_string};
3use crate::render::{Alignment, Column, Columns, Grid, Row, Value};
4use crate::{DagChartConfig, Dimensions, Schema, View};
5use crate::{Flat, Render};
6use std::collections::{HashMap, HashSet};
7use std::fmt::{Debug, Display};
8use std::hash::Hash;
9use std::marker::PhantomData;
10// We use this in the doc strings.
11#[allow(unused_imports)]
12use crate::PathChart;
13
14/// The dag-chart widget.
15///
16/// A dag-chart represents each unique tuple of the view's display dimensions as a path in a directed acyclic graph.
17/// Paths with common sub-paths, starting from the primary dimension (1st), are collapsed in the frame.
18///
19/// See also: [`PathChart`]
20///
21/// ```
22/// use flat::*;
23///
24/// let schema = Schemas::two("Animal", "Size");
25/// let dataset = DatasetBuilder::new(schema)
26///     .add(("whale".to_string(), "large".to_string()))
27///     .add(("shark".to_string(), "medium".to_string()))
28///     .add(("shark".to_string(), "small".to_string()))
29///     .add(("tiger".to_string(), "medium".to_string()))
30///     .add(("tiger".to_string(), "medium".to_string()))
31///     .add(("tiger".to_string(), "small".to_string()))
32///     .build();
33/// let view = dataset.count();
34/// let flat = DagChart::new(&view)
35///     .render(Render::default());
36/// assert_eq!(
37///     format!("\n{}", flat.to_string()),
38///     r#"
39/// Size      Animal  |Sum(Count)
40/// medium  - shark   |**
41/// small   ┘
42/// medium  - tiger   |***
43/// small   ┘
44/// large   - whale   |*"#);
45/// ```
46pub struct DagChart<'a, S, V>
47where
48    S: Schema,
49    V: View<S>,
50{
51    view: &'a V,
52    _phantom: PhantomData<S>,
53}
54
55impl<'a, S, V> DagChart<'a, S, V>
56where
57    S: Schema,
58    V: View<S>,
59    <V as View<S>>::PrimaryDimension: Clone + PartialEq + Eq + Hash,
60    <V as View<S>>::BreakdownDimension: Clone + Display + PartialEq + Eq + Hash + Ord,
61    <V as View<S>>::DisplayDimensions: Clone + PartialEq + Eq + Hash + Ord,
62{
63    /// Construct a dag-chart widget from the provided view.
64    pub fn new(view: &'a V) -> Self {
65        Self {
66            view,
67            _phantom: PhantomData::default(),
68        }
69    }
70
71    /// Generate the flat rendering for this dag-chart.
72    pub fn render(self, config: Render<DagChartConfig>) -> Flat {
73        let mut aggregate_values: HashMap<(V::PrimaryDimension, V::BreakdownDimension), Vec<f64>> =
74            HashMap::default();
75        let mut partial_aggregate_values: HashMap<String, Vec<f64>> = HashMap::default();
76        let mut full_paths: HashSet<String> = HashSet::default();
77        let mut display_dimensions: Vec<V::DisplayDimensions> = Vec::default();
78        let mut sort_breakdowns: Vec<V::BreakdownDimension> = Vec::default();
79        let mut lookup: HashMap<
80            V::DisplayDimensions,
81            (V::PrimaryDimension, V::BreakdownDimension),
82        > = HashMap::default();
83        let mut dimension_values: Vec<HashSet<String>> = (0..self.view.display_headers().len())
84            .map(|_| HashSet::default())
85            .collect();
86        let mut path_occurrences: HashMap<String, usize> = HashMap::default();
87
88        for dims in self.view.dataset().data() {
89            let value = self.view.value(dims);
90            let primary_dim = self.view.primary_dim(dims);
91            let breakdown_dims = self.view.breakdown_dim(dims);
92            let aggregate_dims = (primary_dim.clone(), breakdown_dims.clone());
93            let display_dims = self.view.display_dims(dims);
94            let full_path = display_dims
95                .as_strings()
96                .iter()
97                .fold(String::default(), |acc, part| acc + part + ";");
98
99            for (j, value) in display_dims.as_strings().into_iter().enumerate() {
100                dimension_values[j].insert(value);
101            }
102
103            // Only count the occurrences once per 'full path'.
104            // This is because we might have multiple entries, for example:
105            // ```
106            // DatasetBuilder::new(schema)
107            //     .add(("whale".to_string(), 4u32), 2)
108            //     .add(("whale".to_string(), 4u32), 3)
109            // ```
110            if !full_paths.contains(&full_path) {
111                full_paths.insert(full_path);
112
113                for dag_index in 0..display_dims.len() {
114                    let partial_path = display_dims.as_strings()[0..dag_index + 1]
115                        .iter()
116                        .fold(String::default(), |acc, part| acc + part + ";");
117                    path_occurrences
118                        .entry(partial_path)
119                        .and_modify(|c| *c += 1)
120                        .or_insert(1);
121                }
122            }
123
124            if config.widget_config.show_aggregate {
125                for dag_index in 1..display_dims.len() {
126                    let partial_path = display_dims.as_strings()[0..dag_index + 1]
127                        .iter()
128                        .fold(String::default(), |acc, part| acc + part + ";");
129                    let values = partial_aggregate_values.entry(partial_path).or_default();
130                    values.push(value);
131                }
132            }
133
134            let values = aggregate_values.entry(aggregate_dims.clone()).or_default();
135            values.push(value);
136
137            if !lookup.contains_key(&display_dims) {
138                // Notice, the breakdown_dim will be different in the case of an `is_breakdown` schema.
139                // But in that case, we don't actually use the breakdown from `lookup`.
140                // We really only need this so we can get the `Nothing` breakdown for non-`is_breakdown` schemas.
141                lookup.insert(
142                    display_dims.clone(),
143                    (primary_dim.clone(), breakdown_dims.clone()),
144                );
145                display_dimensions.push(display_dims);
146            }
147
148            if !sort_breakdowns.contains(&breakdown_dims) {
149                sort_breakdowns.push(breakdown_dims);
150            }
151        }
152
153        display_dimensions.sort();
154        sort_breakdowns.sort();
155
156        let mut dimension_abbreviations: Vec<HashMap<String, String>> =
157            (0..self.view.display_headers().len())
158                .map(|_| HashMap::default())
159                .collect();
160
161        if config.widget_config.abbreviate {
162            let headers = self.view.display_headers();
163            let max_header_length = headers.iter().map(|h| h.chars().count()).max().unwrap();
164
165            for (dag_index, values) in dimension_values.iter().enumerate() {
166                let min_header_length = headers[dag_index].to_string().chars().count();
167                let (_, abbreviations) =
168                    find_abbreviations(min_header_length, max_header_length, values);
169                dimension_abbreviations[dag_index] = abbreviations;
170            }
171        }
172
173        let mut columns = Columns::default();
174
175        for j in 0..self.view.display_headers().len() {
176            // dimension value
177            columns.push(Column::string(Alignment::Left));
178
179            if j + 1 < self.view.display_headers().len() {
180                // spacer " "
181                columns.push(Column::string(Alignment::Center));
182
183                if config.widget_config.show_aggregate {
184                    // total left [
185                    columns.push(Column::string(Alignment::Left));
186                    // total value
187                    columns.push(Column::string(Alignment::Right));
188                    // total right ]
189                    columns.push(Column::string(Alignment::Left));
190                }
191
192                // dag marker " . "
193                columns.push(Column::string(Alignment::Center));
194            }
195        }
196
197        if config.show_aggregate {
198            // spacer " "
199            columns.push(Column::string(Alignment::Center));
200            // total left [
201            columns.push(Column::string(Alignment::Left));
202            // total value
203            columns.push(Column::string(Alignment::Right));
204            // total right ]
205            columns.push(Column::string(Alignment::Left));
206        }
207
208        // spacer "  "
209        columns.push(Column::string(Alignment::Center));
210        // rendering delimiter |
211        columns.push(Column::string(Alignment::Center));
212
213        if self.view.breakdown_label().is_some() {
214            for i in 0..sort_breakdowns.len() {
215                // aggregate count
216                columns.push(Column::breakdown(Alignment::Center));
217
218                if i + 1 < sort_breakdowns.len() {
219                    // spacer " "
220                    columns.push(Column::string(Alignment::Left));
221                }
222            }
223
224            // breakdown right |
225            columns.push(Column::string(Alignment::Center));
226        } else {
227            // aggregate count
228            columns.push(Column::count(Alignment::Left));
229        }
230
231        let mut grid = Grid::new(columns);
232
233        if let Some(breakdown_header) = self.view.breakdown_label() {
234            let value_label = self.view.value_label();
235
236            if value_label == breakdown_header {
237                let pre_header = build_preheader(
238                    &config,
239                    self.view.display_headers().len(),
240                    &breakdown_header,
241                    true,
242                );
243                grid.add(pre_header);
244            } else {
245                let pre_header1 = build_preheader(
246                    &config,
247                    self.view.display_headers().len(),
248                    &breakdown_header,
249                    false,
250                );
251                grid.add(pre_header1);
252                let pre_header2 = build_preheader(
253                    &config,
254                    self.view.display_headers().len(),
255                    &value_label,
256                    true,
257                );
258                grid.add(pre_header2);
259            }
260        }
261
262        let mut header = Row::default();
263
264        for (j, name) in self.view.display_headers().iter().rev().enumerate() {
265            header.push(Value::String(name.clone()));
266
267            if j + 1 < self.view.display_headers().len() {
268                header.push(Value::String(" ".to_string()));
269
270                if config.widget_config.show_aggregate {
271                    header.push(Value::Overflow(config.aggregate.to_string()));
272                    header.push(Value::Skip);
273                    header.push(Value::Skip);
274                }
275
276                // For the dag marker " . "
277                header.push(Value::String("   ".to_string()));
278            }
279        }
280
281        if config.show_aggregate {
282            header.push(Value::Empty);
283            header.push(Value::Overflow(config.aggregate.to_string()));
284            header.push(Value::Skip);
285            header.push(Value::Skip);
286        }
287
288        header.push(Value::String("  ".to_string()));
289        header.push(Value::String("|".to_string()));
290
291        if self.view.breakdown_label().is_some() {
292            for (k, breakdown_dim) in sort_breakdowns.iter().enumerate() {
293                header.push(Value::String(breakdown_dim.to_string()));
294
295                if k + 1 < sort_breakdowns.len() {
296                    header.push(Value::String(" ".to_string()));
297                }
298            }
299
300            header.push(Value::String("|".to_string()));
301        } else {
302            header.push(Value::Plain(format!(
303                "{}({})",
304                config.aggregate.to_string(),
305                self.view.value_label()
306            )));
307            // header.push(Value::Plain(config.aggregate.to_string()));
308        }
309
310        grid.add(header);
311        let mut column_groups: HashMap<usize, Group> = HashMap::default();
312        let mut minimum_value = f64::MAX;
313        let mut maximum_value = f64::MIN;
314
315        for display_dims in display_dimensions.iter() {
316            let path = display_dims.as_strings();
317            let mut column_chunks_reversed: Vec<Vec<Value>> = Vec::default();
318            let mut descendant_position = None;
319
320            #[allow(unused_doc_comments)]
321            /// Run through the path in dag index ascending order, which
322            /// is the "rendering" reverse order.
323            ///
324            /// For this example dag, we'll iterate as follows:
325            /// a - b ┐
326            /// c ┐   - d
327            /// e - f ┘
328            /// h ┘
329            ///
330            /// path: ["d", "b", "a"]
331            /// dag_index | j | part | partial_path
332            /// ------------------------------
333            /// 0        | 2 | "d"   | "d"
334            /// 1        | 1 | "b"   | "d;b"
335            /// 2        | 0 | "a"   | "d;b;a"
336            ///
337            /// path: ["d", "f", "c"]
338            /// dag_index | j | part | partial_path
339            /// ------------------------------
340            /// 0        | 2 | "d"   | "d"
341            /// 1        | 1 | "f"   | "d;f"
342            /// 2        | 0 | "c"   | "d;f;c"
343            ///
344            /// path: ["d", "f", "e"]
345            /// dag_index | j | part | partial_path
346            /// ------------------------------
347            /// 0        | 2 | "d"   | "d"
348            /// 1        | 1 | "f"   | "d;f"
349            /// 2        | 0 | "e"   | "d;f;e"
350            ///
351            /// etc..
352            ///
353            for (dag_index, part) in path.clone().iter().enumerate() {
354                let j = path.len() - dag_index - 1;
355                let partial_path = path[0..dag_index + 1]
356                    .iter()
357                    .fold(String::default(), |acc, part| acc + part + ";");
358                let group = column_groups.entry(j).or_default();
359
360                if group.matches(&partial_path) {
361                    group.increment();
362                } else {
363                    group.swap(partial_path.clone());
364                }
365
366                let occurrences = path_occurrences[&partial_path];
367                let position = (occurrences as f64 / 2.0).ceil() as usize - 1;
368                let mut column_chunks = Vec::default();
369
370                let position = match position {
371                    position if position > group.index => Position::Above,
372                    position if position == group.index => Position::At,
373                    _ => Position::Below,
374                };
375
376                if position == Position::At {
377                    if config.widget_config.abbreviate {
378                        column_chunks.push(Value::String(
379                            dimension_abbreviations[dag_index][part].clone(),
380                        ));
381                    } else {
382                        column_chunks.push(Value::String(part.clone()));
383                    }
384
385                    if dag_index == 0 {
386                        let (primary_dim, breakdown_dim) = lookup
387                            .get(display_dims)
388                            .expect("sort dimensions must be mapped to dimensions");
389
390                        if self.view.breakdown_label().is_some() {
391                            let breakdown_values: Vec<f64> = sort_breakdowns
392                                .iter()
393                                .map(|breakdown_dim| {
394                                    let aggregate_dims =
395                                        (primary_dim.clone(), breakdown_dim.clone());
396                                    aggregate_apply(
397                                        &config.aggregate,
398                                        &aggregate_values,
399                                        &aggregate_dims,
400                                        &mut minimum_value,
401                                        &mut maximum_value,
402                                    )
403                                })
404                                .collect();
405
406                            if config.show_aggregate {
407                                column_chunks.push(Value::String(" ".to_string()));
408                                column_chunks.push(Value::String("[".to_string()));
409                                column_chunks.push(Value::String(minimal_precision_string(
410                                    config.aggregate.apply(breakdown_values.as_slice()),
411                                )));
412                                column_chunks.push(Value::String("]".to_string()));
413                            }
414
415                            column_chunks.push(Value::String("  ".to_string()));
416                            column_chunks.push(Value::String("|".to_string()));
417
418                            for (k, breakdown_value) in breakdown_values.iter().enumerate() {
419                                column_chunks.push(Value::Value(*breakdown_value));
420
421                                if k + 1 != breakdown_values.len() {
422                                    column_chunks.push(Value::String(" ".to_string()));
423                                }
424                            }
425
426                            column_chunks.push(Value::String("|".to_string()));
427                        } else {
428                            let aggregate_dims = (primary_dim.clone(), breakdown_dim.clone());
429                            let value = aggregate_apply(
430                                &config.aggregate,
431                                &aggregate_values,
432                                &aggregate_dims,
433                                &mut minimum_value,
434                                &mut maximum_value,
435                            );
436
437                            if config.show_aggregate {
438                                column_chunks.push(Value::String(" ".to_string()));
439                                column_chunks.push(Value::String("[".to_string()));
440                                column_chunks.push(Value::String(minimal_precision_string(value)));
441                                column_chunks.push(Value::String("]".to_string()));
442                            }
443
444                            column_chunks.push(Value::String("  ".to_string()));
445                            column_chunks.push(Value::String("|".to_string()));
446                            column_chunks.push(Value::Value(value));
447
448                            if value < minimum_value {
449                                minimum_value = value;
450                            }
451
452                            if value > maximum_value {
453                                maximum_value = value;
454                            }
455                        }
456                    } else {
457                        assert!(descendant_position.is_some());
458                        column_chunks.push(Value::String(" ".to_string()));
459
460                        if config.widget_config.show_aggregate {
461                            let value = config
462                                .aggregate
463                                .apply(partial_aggregate_values[&partial_path].as_slice());
464                            column_chunks.push(Value::String("[".to_string()));
465                            column_chunks.push(Value::String(minimal_precision_string(value)));
466                            column_chunks.push(Value::String("]".to_string()));
467                        }
468
469                        if let Some(desc_pos) = &descendant_position {
470                            match desc_pos {
471                                Position::Above => {
472                                    column_chunks.push(Value::String(format!("┐")));
473                                }
474                                Position::At => {
475                                    column_chunks.push(Value::String(format!("-")));
476                                }
477                                Position::Below => {
478                                    column_chunks.push(Value::String(format!("┘")));
479                                }
480                            }
481                        }
482                    }
483                } else if dag_index != 0 {
484                    assert!(descendant_position.is_some());
485
486                    if let Some(desc_pos) = &descendant_position {
487                        match desc_pos {
488                            Position::At => {
489                                column_chunks.push(Value::Empty);
490                                column_chunks.push(Value::String(" ".to_string()));
491
492                                if config.widget_config.show_aggregate {
493                                    column_chunks.push(Value::Empty);
494                                    column_chunks.push(Value::Empty);
495                                    column_chunks.push(Value::Empty);
496                                }
497                                column_chunks.push(Value::String(format!("-")));
498                            }
499                            Position::Above | Position::Below => {
500                                // TODO: handle this case
501                            }
502                        }
503                    }
504                }
505
506                descendant_position.replace(position);
507                column_chunks_reversed.push(column_chunks);
508            }
509
510            let mut row = Row::default();
511
512            for column_chunks in column_chunks_reversed.into_iter().rev() {
513                for value in column_chunks.into_iter() {
514                    row.push(value);
515                }
516            }
517
518            grid.add(row);
519        }
520
521        Flat::new(config, minimum_value..maximum_value, grid)
522    }
523}
524
525fn build_preheader(
526    config: &Render<DagChartConfig>,
527    columns: usize,
528    label: &str,
529    embed: bool,
530) -> Row {
531    let mut row = Row::default();
532
533    for j in 0..columns {
534        row.push(Value::Empty);
535
536        if j + 1 < columns {
537            row.push(Value::Empty);
538
539            if config.widget_config.show_aggregate {
540                row.push(Value::Empty);
541                row.push(Value::Empty);
542                row.push(Value::Empty);
543            }
544
545            row.push(Value::Empty);
546        }
547    }
548
549    if config.show_aggregate {
550        row.push(Value::Empty);
551        row.push(Value::Empty);
552        row.push(Value::Empty);
553        row.push(Value::Empty);
554    }
555
556    row.push(Value::Empty);
557    row.push(Value::Empty);
558
559    if embed {
560        row.push(Value::Plain(format!(
561            "{}({label})",
562            config.aggregate.to_string(),
563        )));
564    } else {
565        row.push(Value::Plain(format!("{label}")));
566    }
567
568    row
569}
570
571#[derive(Debug, PartialEq, Eq)]
572enum Position {
573    Above,
574    At,
575    Below,
576}
577
578#[derive(Debug)]
579struct Group {
580    locus: Option<String>,
581    index: usize,
582}
583
584impl Default for Group {
585    fn default() -> Self {
586        Self {
587            locus: None,
588            index: 0,
589        }
590    }
591}
592
593impl Group {
594    fn matches(&self, path: &String) -> bool {
595        match &self.locus {
596            Some(l) => l == path,
597            None => false,
598        }
599    }
600
601    fn swap(&mut self, locus: String) {
602        self.locus.replace(locus);
603        self.index = 0;
604    }
605
606    fn increment(&mut self) {
607        self.index += 1;
608    }
609}
610
611#[cfg(test)]
612mod tests {
613
614    #[cfg(feature = "primitive_impls")]
615    mod primitive_impls {
616        use crate::{DagChart, DagChartConfig, Render};
617        use crate::{DatasetBuilder, Schema1, Schema2, Schemas};
618
619        #[test]
620        fn empty() {
621            let schema: Schema1<i64> = Schemas::one("abc");
622            let builder = DatasetBuilder::new(schema).build();
623            let view = builder.reflect_1st();
624            let dagchart = DagChart::new(&view);
625            let flat = dagchart.render(Render::default());
626            assert_eq!(
627                format!("\n{}", flat.to_string()),
628                r#"
629abc  |Sum(abc)"#
630            );
631        }
632
633        #[test]
634        fn zero() {
635            let schema: Schema1<i64> = Schemas::one("abc");
636            let dataset = DatasetBuilder::new(schema).add((0,)).build();
637            let view = dataset.reflect_1st();
638            let dagchart = DagChart::new(&view);
639            let flat = dagchart.render(Render::default());
640            assert_eq!(
641                format!("\n{}", flat.to_string()),
642                r#"
643abc  |Sum(abc)
6440    |"#
645            );
646        }
647
648        #[test]
649        fn negatives_and_positives() {
650            let schema: Schema1<i64> = Schemas::one("abc");
651            let dataset = DatasetBuilder::new(schema)
652                .add((-1,))
653                .add((0,))
654                .add((1,))
655                .build();
656            let view = dataset.reflect_1st();
657            let dagchart = DagChart::new(&view);
658            let flat = dagchart.render(Render::default());
659            assert_eq!(
660                format!("\n{}", flat.to_string()),
661                r#"
662abc  |Sum(abc)
663-1   |⊖
6640    |
6651    |*"#
666            );
667        }
668
669        #[test]
670        fn one_thousand() {
671            let schema: Schema1<i64> = Schemas::one("abc");
672            let mut builder = DatasetBuilder::new(schema);
673
674            for _ in 0..1_000 {
675                builder.update((1,));
676            }
677
678            let dataset = builder.build();
679            let view = dataset.reflect_1st();
680            let dagchart = DagChart::new(&view);
681            let flat = dagchart.render(Render::default());
682            assert_eq!(
683                format!("\n{}", flat.to_string()),
684                r#"
685abc  |Sum(abc)
6861    |**********************************************************************************************************************************************************"#
687            );
688        }
689
690        #[test]
691        fn negative_one_thousand() {
692            let schema: Schema1<i64> = Schemas::one("abc");
693            let mut builder = DatasetBuilder::new(schema);
694
695            for _ in 0..1_000 {
696                builder.update((-1,));
697            }
698
699            let dataset = builder.build();
700            let view = dataset.reflect_1st();
701            let dagchart = DagChart::new(&view);
702            let flat = dagchart.render(Render::default());
703            assert_eq!(
704                format!("\n{}", flat.to_string()),
705                r#"
706abc  |Sum(abc)
707-1   |⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖⊖"#
708            );
709        }
710
711        #[test]
712        fn breakdown() {
713            let schema: Schema2<u8, u8> = Schemas::two("abc", "something long");
714            let dataset = DatasetBuilder::new(schema)
715                .add((1, 2))
716                .add((2, 3))
717                .add((3, 4))
718                .build();
719            let view = dataset.breakdown_2nd();
720            let dagchart = DagChart::new(&view);
721            let flat = dagchart.render(Render::default());
722            assert_eq!(
723                format!("\n{}", flat.to_string()),
724                r#"
725      Sum(something long)
726abc  | 2    3    4  |
7271    | **           |
7282    |     ***      |
7293    |          ****|"#
730            );
731        }
732
733        #[test]
734        fn count_breakdown() {
735            let schema = Schemas::two("abc", "something long");
736            let dataset = DatasetBuilder::new(schema)
737                .add((1, 2))
738                .add((2, 3))
739                .add((3, 4))
740                .build();
741            let view = dataset.count_breakdown_2nd();
742            let dagchart = DagChart::new(&view);
743            let flat = dagchart.render(Render::default());
744            assert_eq!(
745                format!("\n{}", flat.to_string()),
746                r#"
747      something long
748      Sum(Count)
749abc  |2 3 4|
7501    |*    |
7512    |  *  |
7523    |    *|"#
753            );
754        }
755
756        #[test]
757        fn depth_3_combo111() {
758            let schema = Schemas::three("A", "B", "C");
759            let dataset = DatasetBuilder::new(schema).add(("a1", "b1", "c1")).build();
760            let view = dataset.count();
761            let dagchart = DagChart::new(&view);
762            let flat = dagchart.render(Render {
763                show_aggregate: true,
764                widget_config: DagChartConfig {
765                    show_aggregate: true,
766                    ..DagChartConfig::default()
767                },
768                ..Render::default()
769            });
770            assert_eq!(
771                format!("\n{}", flat.to_string()),
772                r#"
773C  Sum   B  Sum   A  Sum  |Sum(Count)
774c1 [1] - b1 [1] - a1 [1]  |*"#
775            );
776        }
777
778        #[test]
779        fn depth_3_combo211() {
780            let schema = Schemas::three("A", "B", "C");
781            let dataset = DatasetBuilder::new(schema)
782                .add(("a1", "b1", "c1"))
783                .add(("a1", "b1", "c2"))
784                .build();
785            let view = dataset.count();
786            let dagchart = DagChart::new(&view);
787            let flat = dagchart.render(Render {
788                show_aggregate: true,
789                widget_config: DagChartConfig {
790                    show_aggregate: true,
791                    ..DagChartConfig::default()
792                },
793                ..Render::default()
794            });
795            assert_eq!(
796                format!("\n{}", flat.to_string()),
797                r#"
798C  Sum   B  Sum   A  Sum  |Sum(Count)
799c1 [1] - b1 [2] - a1 [2]  |**
800c2 [1] ┘"#
801            );
802        }
803
804        #[test]
805        fn depth_3_combo221() {
806            let schema = Schemas::three("A", "B", "C");
807            let dataset = DatasetBuilder::new(schema)
808                .add(("a1", "b1", "c1"))
809                .add(("a1", "b1", "c2"))
810                .add(("a1", "b2", "c2"))
811                .build();
812            let view = dataset.count();
813            let dagchart = DagChart::new(&view);
814            let flat = dagchart.render(Render {
815                show_aggregate: true,
816                widget_config: DagChartConfig {
817                    show_aggregate: true,
818                    ..DagChartConfig::default()
819                },
820                ..Render::default()
821            });
822            assert_eq!(
823                format!("\n{}", flat.to_string()),
824                r#"
825C  Sum   B  Sum   A  Sum  |Sum(Count)
826c1 [1] - b1 [2] ┐
827c2 [1] ┘        - a1 [3]  |***
828c2 [1] - b2 [1] ┘"#
829            );
830        }
831
832        #[test]
833        fn depth_3_combo221x() {
834            let schema = Schemas::three("A", "B", "C");
835            let dataset = DatasetBuilder::new(schema)
836                .add(("a1", "b1", "c1"))
837                .add(("a1", "b1", "c2"))
838                .add(("a1", "b2", "c1"))
839                .build();
840            let view = dataset.count();
841            let dagchart = DagChart::new(&view);
842            let flat = dagchart.render(Render {
843                show_aggregate: true,
844                widget_config: DagChartConfig {
845                    show_aggregate: true,
846                    ..DagChartConfig::default()
847                },
848                ..Render::default()
849            });
850            assert_eq!(
851                format!("\n{}", flat.to_string()),
852                r#"
853C  Sum   B  Sum   A  Sum  |Sum(Count)
854c1 [1] - b1 [2] ┐
855c2 [1] ┘        - a1 [3]  |***
856c1 [1] - b2 [1] ┘"#
857            );
858        }
859
860        #[test]
861        fn depth_3_combo311() {
862            let schema = Schemas::three("A", "B", "C");
863            let dataset = DatasetBuilder::new(schema)
864                .add(("a1", "b1", "c1"))
865                .add(("a1", "b1", "c2"))
866                .add(("a1", "b1", "c3"))
867                .build();
868            let view = dataset.count();
869            let dagchart = DagChart::new(&view);
870            let flat = dagchart.render(Render {
871                show_aggregate: true,
872                widget_config: DagChartConfig {
873                    show_aggregate: true,
874                    ..DagChartConfig::default()
875                },
876                ..Render::default()
877            });
878            assert_eq!(
879                format!("\n{}", flat.to_string()),
880                r#"
881C  Sum   B  Sum   A  Sum  |Sum(Count)
882c1 [1] ┐
883c2 [1] - b1 [3] - a1 [3]  |***
884c3 [1] ┘"#
885            );
886        }
887
888        #[test]
889        fn depth_3_combo321() {
890            let schema = Schemas::three("A", "B", "C");
891            let dataset = DatasetBuilder::new(schema)
892                .add(("a1", "b1", "c1"))
893                .add(("a1", "b1", "c2"))
894                .add(("a1", "b1", "c3"))
895                .add(("a1", "b2", "c3"))
896                .build();
897            let view = dataset.count();
898            let dagchart = DagChart::new(&view);
899            let flat = dagchart.render(Render {
900                show_aggregate: true,
901                widget_config: DagChartConfig {
902                    show_aggregate: true,
903                    ..DagChartConfig::default()
904                },
905                ..Render::default()
906            });
907            assert_eq!(
908                format!("\n{}", flat.to_string()),
909                r#"
910C  Sum   B  Sum   A  Sum  |Sum(Count)
911c1 [1] ┐
912c2 [1] - b1 [3] - a1 [4]  |****
913c3 [1] ┘
914c3 [1] - b2 [1] ┘"#
915            );
916        }
917
918        #[test]
919        fn depth_3_combo321x() {
920            let schema = Schemas::three("A", "B", "C");
921            let dataset = DatasetBuilder::new(schema)
922                .add(("a1", "b1", "c1"))
923                .add(("a1", "b1", "c2"))
924                .add(("a1", "b1", "c3"))
925                .add(("a1", "b2", "c2"))
926                .build();
927            let view = dataset.count();
928            let dagchart = DagChart::new(&view);
929            let flat = dagchart.render(Render {
930                show_aggregate: true,
931                widget_config: DagChartConfig {
932                    show_aggregate: true,
933                    ..DagChartConfig::default()
934                },
935                ..Render::default()
936            });
937            assert_eq!(
938                format!("\n{}", flat.to_string()),
939                r#"
940C  Sum   B  Sum   A  Sum  |Sum(Count)
941c1 [1] ┐
942c2 [1] - b1 [3] - a1 [4]  |****
943c3 [1] ┘
944c2 [1] - b2 [1] ┘"#
945            );
946        }
947
948        #[test]
949        fn depth_3_combo321y() {
950            let schema = Schemas::three("A", "B", "C");
951            let dataset = DatasetBuilder::new(schema)
952                .add(("a1", "b1", "c1"))
953                .add(("a1", "b1", "c2"))
954                .add(("a1", "b1", "c3"))
955                .add(("a1", "b2", "c1"))
956                .build();
957            let view = dataset.count();
958            let dagchart = DagChart::new(&view);
959            let flat = dagchart.render(Render {
960                show_aggregate: true,
961                widget_config: DagChartConfig {
962                    show_aggregate: true,
963                    ..DagChartConfig::default()
964                },
965                ..Render::default()
966            });
967            assert_eq!(
968                format!("\n{}", flat.to_string()),
969                r#"
970C  Sum   B  Sum   A  Sum  |Sum(Count)
971c1 [1] ┐
972c2 [1] - b1 [3] - a1 [4]  |****
973c3 [1] ┘
974c1 [1] - b2 [1] ┘"#
975            );
976        }
977
978        #[test]
979        fn depth_3_combo331() {
980            let schema = Schemas::three("A", "B", "C");
981            let dataset = DatasetBuilder::new(schema)
982                .add(("a1", "b1", "c1"))
983                .add(("a1", "b1", "c2"))
984                .add(("a1", "b1", "c3"))
985                .add(("a1", "b2", "c1"))
986                .add(("a1", "b3", "c1"))
987                .build();
988            let view = dataset.count();
989            let dagchart = DagChart::new(&view);
990            let flat = dagchart.render(Render {
991                show_aggregate: true,
992                widget_config: DagChartConfig {
993                    show_aggregate: true,
994                    ..DagChartConfig::default()
995                },
996                ..Render::default()
997            });
998            assert_eq!(
999                format!("\n{}", flat.to_string()),
1000                r#"
1001C  Sum   B  Sum   A  Sum  |Sum(Count)
1002c1 [1] ┐
1003c2 [1] - b1 [3] ┐
1004c3 [1] ┘        - a1 [5]  |*****
1005c1 [1] - b2 [1] ┘
1006c1 [1] - b3 [1] ┘"#
1007            );
1008        }
1009    }
1010
1011    #[cfg(feature = "pointer_impls")]
1012    mod pointer_impls {
1013        use crate::{DagChart, Render};
1014        use crate::{DatasetBuilder, Schema2, Schemas};
1015        use ordered_float::OrderedFloat;
1016
1017        #[test]
1018        fn view2() {
1019            let schema: Schema2<i64, OrderedFloat<f64>> = Schemas::two("abc", "def");
1020            let dataset = DatasetBuilder::new(schema)
1021                .add((1, OrderedFloat(0.1)))
1022                .add((2, OrderedFloat(0.4)))
1023                .add((3, OrderedFloat(0.5)))
1024                .add((4, OrderedFloat(0.9)))
1025                .build();
1026            let view = dataset.view_2nd();
1027            let dagchart = DagChart::new(&view);
1028            let flat = dagchart.render(Render::default());
1029            assert_eq!(
1030                format!("\n{}", flat.to_string()),
1031                r#"
1032abc  |Sum(def)
10331    |
10342    |
10353    |*
10364    |*"#
1037            );
1038
1039            let view = dataset.count();
1040            let dagchart = DagChart::new(&view);
1041            let flat = dagchart.render(Render::default());
1042            assert_eq!(
1043                format!("\n{}", flat.to_string()),
1044                r#"
1045def    abc  |Sum(Count)
10460.1  - 1    |*
10470.4  - 2    |*
10480.5  - 3    |*
10490.9  - 4    |*"#
1050            );
1051
1052            let view = dataset.breakdown_2nd();
1053            let dagchart = DagChart::new(&view);
1054            let flat = dagchart.render(Render::default());
1055            assert_eq!(
1056                format!("\n{}", flat.to_string()),
1057                r#"
1058      Sum(def)
1059abc  |0.1 0.4 0.5 0.9|
10601    |               |
10612    |               |
10623    |         *     |
10634    |             * |"#
1064            );
1065
1066            let view = dataset.count_breakdown_2nd();
1067            let dagchart = DagChart::new(&view);
1068            let flat = dagchart.render(Render::default());
1069            assert_eq!(
1070                format!("\n{}", flat.to_string()),
1071                r#"
1072      def
1073      Sum(Count)
1074abc  |0.1 0.4 0.5 0.9|
10751    | *             |
10762    |     *         |
10773    |         *     |
10784    |             * |"#
1079            );
1080        }
1081    }
1082}