Skip to main content

fret_ui_headless/table/
faceting.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use super::{
5    ColumnDef, ColumnFilter, FilterFnDef, FilteringFnSpec, GlobalFilterState, RowModel,
6    TableOptions,
7};
8
9pub type FacetKey = u64;
10pub type FacetCounts = HashMap<FacetKey, usize>;
11
12pub type FacetLabels<'a> = HashMap<FacetKey, &'a str>;
13
14pub fn faceted_row_model_excluding<'a, TData>(
15    pre_filtered: &RowModel<'a, TData>,
16    columns: &[ColumnDef<TData>],
17    column_filters: &[ColumnFilter],
18    global_filter: GlobalFilterState,
19    options: TableOptions,
20    filter_fns: &HashMap<Arc<str>, FilterFnDef>,
21    global_filter_fn: &FilteringFnSpec,
22    get_column_can_global_filter: Option<&dyn Fn(&ColumnDef<TData>, &TData) -> bool>,
23    exclude_column_id: Option<&str>,
24) -> RowModel<'a, TData> {
25    let other_filters: Vec<ColumnFilter> = column_filters
26        .iter()
27        .filter(|f| exclude_column_id.is_none_or(|id| f.column.as_ref() != id))
28        .cloned()
29        .collect();
30    super::filter_row_model(
31        pre_filtered,
32        columns,
33        &other_filters,
34        global_filter,
35        options,
36        filter_fns,
37        global_filter_fn,
38        get_column_can_global_filter,
39    )
40}
41
42pub fn faceted_unique_values<'a, TData>(
43    row_model: &RowModel<'a, TData>,
44    column: &ColumnDef<TData>,
45) -> FacetCounts {
46    let Some(facet_key_fn) = column.facet_key_fn.as_ref() else {
47        return FacetCounts::new();
48    };
49
50    let mut counts: FacetCounts = FacetCounts::new();
51    for &row_index in row_model.flat_rows() {
52        let Some(row) = row_model.row(row_index) else {
53            continue;
54        };
55        let key = (facet_key_fn)(row.original);
56        *counts.entry(key).or_insert(0) += 1;
57    }
58    counts
59}
60
61pub fn faceted_unique_value_labels<'a, TData>(
62    row_model: &RowModel<'a, TData>,
63    column: &ColumnDef<TData>,
64) -> FacetLabels<'a> {
65    let Some(facet_key_fn) = column.facet_key_fn.as_ref() else {
66        return FacetLabels::new();
67    };
68    let Some(facet_str_fn) = column.facet_str_fn.as_ref() else {
69        return FacetLabels::new();
70    };
71
72    let mut labels: FacetLabels<'a> = FacetLabels::new();
73    for &row_index in row_model.flat_rows() {
74        let Some(row) = row_model.row(row_index) else {
75            continue;
76        };
77        let key = (facet_key_fn)(row.original);
78        labels
79            .entry(key)
80            .or_insert_with(|| (facet_str_fn)(row.original));
81    }
82    labels
83}
84
85pub fn faceted_min_max_u64<'a, TData>(
86    row_model: &RowModel<'a, TData>,
87    column: &ColumnDef<TData>,
88) -> Option<(u64, u64)> {
89    let facet_key_fn = column.facet_key_fn.as_ref()?;
90
91    let mut iter = row_model
92        .flat_rows()
93        .iter()
94        .filter_map(|&i| row_model.row(i).map(|r| (facet_key_fn)(r.original)));
95
96    let first = iter.next()?;
97    let mut min = first;
98    let mut max = first;
99    for v in iter {
100        min = min.min(v);
101        max = max.max(v);
102    }
103    Some((min, max))
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::table::{ColumnDef, ColumnFilter, Table, TableState, create_column_helper};
110    use std::sync::Arc;
111
112    #[derive(Debug, Clone)]
113    struct Item {
114        status_key: u64,
115        status_label: Arc<str>,
116        role_key: u64,
117        role_label: Arc<str>,
118    }
119
120    fn build_table(state: TableState) -> Table<'static, Item> {
121        let data: &'static [Item] = Box::leak(
122            vec![
123                Item {
124                    status_key: 1,
125                    status_label: "A".into(),
126                    role_key: 10,
127                    role_label: "X".into(),
128                },
129                Item {
130                    status_key: 2,
131                    status_label: "B".into(),
132                    role_key: 10,
133                    role_label: "X".into(),
134                },
135                Item {
136                    status_key: 1,
137                    status_label: "A".into(),
138                    role_key: 20,
139                    role_label: "Y".into(),
140                },
141            ]
142            .into_boxed_slice(),
143        );
144
145        let helper = create_column_helper::<Item>();
146        let status = ColumnDef::new("status")
147            .filter_by(|it: &Item, q| it.status_label.as_ref() == q)
148            .facet_key_by(|it| it.status_key)
149            .facet_str_by(|it| it.status_label.as_ref());
150        let role = helper
151            .accessor("role", |it| it.role_key)
152            .filter_by(|it: &Item, q| it.role_label.as_ref() == q)
153            .facet_key_by(|it| it.role_key)
154            .facet_str_by(|it| it.role_label.as_ref());
155
156        Table::builder(data)
157            .columns(vec![status, role])
158            .state(state)
159            .build()
160    }
161
162    #[test]
163    fn faceted_row_model_excludes_own_column_filter() {
164        let mut state = TableState::default();
165        state.column_filters = vec![
166            ColumnFilter {
167                column: "status".into(),
168                value: serde_json::Value::from("A"),
169            },
170            ColumnFilter {
171                column: "role".into(),
172                value: serde_json::Value::from("X"),
173            },
174        ];
175        state.global_filter = None;
176
177        let table = build_table(state);
178
179        // exclude status => only role=X applies => rows 0 and 1 remain
180        let model = faceted_row_model_excluding(
181            table.pre_filtered_row_model(),
182            table.columns(),
183            &table.state().column_filters,
184            table.state().global_filter.clone(),
185            TableOptions::default(),
186            &HashMap::new(),
187            &FilteringFnSpec::Auto,
188            None,
189            Some("status"),
190        );
191        let counts = faceted_unique_values(&model, table.column("status").unwrap());
192        assert_eq!(counts.get(&1).copied(), Some(1));
193        assert_eq!(counts.get(&2).copied(), Some(1));
194
195        let labels = faceted_unique_value_labels(&model, table.column("status").unwrap());
196        assert_eq!(labels.get(&1).copied(), Some("A"));
197        assert_eq!(labels.get(&2).copied(), Some("B"));
198    }
199
200    #[test]
201    fn faceted_min_max_uses_flat_rows() {
202        let mut state = TableState::default();
203        state.column_filters = Vec::new();
204        state.global_filter = None;
205
206        let table = build_table(state);
207        let model = table.pre_filtered_row_model();
208        let (min, max) = faceted_min_max_u64(model, table.column("status").unwrap()).unwrap();
209        assert_eq!((min, max), (1, 2));
210    }
211}