Skip to main content

graphrecords_overview/
group.rs

1use crate::{
2    AttributeOverview, AttributeOverviewData, OverviewResult,
3    tabled_modifiers::MergeDuplicatesVerticalByColumn,
4};
5use graphrecords_core::{
6    GraphRecord,
7    errors::GraphRecordError,
8    prelude::{AttributeType, GraphRecordAttribute, GraphRecordValue, Group, GroupSchema},
9};
10use graphrecords_query::{
11    Attribute, Filter, Index, IsIn, IsNull, Maximum, Mean, Minimum, OnErrorOf, QueryEdges,
12    QueryNodes, QueryResult, error::structure::MissingAttribute, operations::policy::Drop,
13};
14use graphrecords_utils::aliases::{GrHashMap, GrHashSet};
15use itertools::Itertools;
16use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
17use std::fmt::{Display, Formatter};
18use tabled::{
19    builder::Builder,
20    settings::{Alignment, Panel, Style, Width, object::Columns, themes::BorderCorrection},
21};
22
23#[derive(Debug, Clone)]
24pub struct NodeGroupOverview {
25    pub count: usize,
26    pub attributes: GrHashMap<GraphRecordAttribute, AttributeOverview>,
27
28    truncate_details: Option<usize>,
29}
30
31impl Display for NodeGroupOverview {
32    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
33        let mut builder = Builder::new();
34
35        builder.push_record([
36            "Node Count",
37            "Attribute",
38            "Attribute Type",
39            "Data Type",
40            "Details",
41        ]);
42
43        for (attribute, overview) in &self.attributes {
44            let details = overview.data.details();
45
46            builder.push_record([
47                &self.count.to_string(),
48                &attribute.to_string(),
49                overview.data.attribute_type_name(),
50                &overview.data_type.to_string(),
51                &details,
52            ]);
53        }
54
55        if self.attributes.is_empty() && self.count > 0 {
56            builder.push_record([&self.count.to_string(), "-", "-", "-", "-"]);
57        }
58
59        let mut table = builder.build();
60        table.with(Style::modern());
61        table.with(Panel::header("Node Overview"));
62        table.with(MergeDuplicatesVerticalByColumn::new(vec![0]));
63        table.with(Alignment::center_vertical());
64        table.with(BorderCorrection {});
65
66        if let Some(truncate_details) = self.truncate_details {
67            table.modify(Columns::last(), Width::truncate(truncate_details));
68        }
69
70        writeln!(f, "{table}")
71    }
72}
73
74impl NodeGroupOverview {
75    fn new(
76        graphrecord: &GraphRecord,
77        group_schema: &GroupSchema,
78        group: Option<&Group>,
79        truncate_details: Option<usize>,
80    ) -> OverviewResult<Self> {
81        let nodes_in_group: GrHashSet<_> = match group {
82            Some(group) => graphrecord.nodes_in_group(group)?.cloned().collect(),
83            None => graphrecord.ungrouped_nodes().cloned().collect(),
84        };
85        let count = nodes_in_group.len();
86
87        let attributes: GrHashMap<_, _> = group_schema
88            .nodes()
89            .par_iter()
90            .map(|(key, attribute_data_type)| {
91                let attribute_type = attribute_data_type.attribute_type();
92                let data_type = attribute_data_type.data_type().clone();
93
94                let attribute_overview = match attribute_type {
95                    AttributeType::Categorical => {
96                        let selection = graphrecord.query_nodes(|nodes| {
97                            let nodes = nodes.filter(nodes.index().is_in(nodes_in_group.clone()));
98
99                            nodes
100                                .attribute(key.clone())
101                                .on_error_of::<MissingAttribute>(Drop)
102                        });
103
104                        let values: Vec<_> = selection
105                            .evaluate()?
106                            .map(|(_, value)| value)
107                            .collect::<QueryResult<_>>()?;
108
109                        AttributeOverview {
110                            data_type,
111                            data: AttributeOverviewData::Categorical {
112                                distinct_values: values
113                                    .into_iter()
114                                    .sorted_by(GraphRecordValue::total_cmp)
115                                    .dedup()
116                                    .collect(),
117                            },
118                        }
119                    }
120                    AttributeType::Continuous => {
121                        let selection = graphrecord.query_nodes(|nodes| {
122                            let nodes = nodes.filter(nodes.index().is_in(nodes_in_group.clone()));
123                            let values = nodes
124                                .attribute(key.clone())
125                                .on_error_of::<MissingAttribute>(Drop);
126                            let values = values.filter(!values.is_null());
127
128                            (values.min(), values.mean(), values.max())
129                        });
130
131                        let (minimum, mean, maximum) = selection.evaluate()?;
132
133                        AttributeOverview {
134                            data_type,
135                            data: AttributeOverviewData::Continuous {
136                                min: minimum.transpose()?.unwrap_or(GraphRecordValue::Null),
137                                mean: mean.transpose()?.unwrap_or(GraphRecordValue::Null),
138                                max: maximum.transpose()?.unwrap_or(GraphRecordValue::Null),
139                            },
140                        }
141                    }
142                    AttributeType::Temporal => {
143                        let selection = graphrecord.query_nodes(|nodes| {
144                            let nodes = nodes.filter(nodes.index().is_in(nodes_in_group.clone()));
145                            let values = nodes
146                                .attribute(key.clone())
147                                .on_error_of::<MissingAttribute>(Drop);
148                            let values = values.filter(!values.is_null());
149
150                            (values.min(), values.max())
151                        });
152
153                        let (minimum, maximum) = selection.evaluate()?;
154
155                        AttributeOverview {
156                            data_type,
157                            data: AttributeOverviewData::Temporal {
158                                min: minimum.transpose()?.unwrap_or(GraphRecordValue::Null),
159                                max: maximum.transpose()?.unwrap_or(GraphRecordValue::Null),
160                            },
161                        }
162                    }
163                    AttributeType::Unstructured => {
164                        let selection = graphrecord.query_nodes(|nodes| {
165                            let nodes = nodes.filter(nodes.index().is_in(nodes_in_group.clone()));
166
167                            nodes
168                                .attribute(key.clone())
169                                .on_error_of::<MissingAttribute>(Drop)
170                        });
171
172                        let values: Vec<_> = selection
173                            .evaluate()?
174                            .map(|(_, value)| value)
175                            .collect::<QueryResult<_>>()?;
176
177                        AttributeOverview {
178                            data_type,
179                            data: AttributeOverviewData::Unstructured {
180                                distinct_count: values
181                                    .into_iter()
182                                    .sorted_by(GraphRecordValue::total_cmp)
183                                    .dedup()
184                                    .count(),
185                            },
186                        }
187                    }
188                };
189
190                Ok((key.clone(), attribute_overview))
191            })
192            .collect::<QueryResult<_>>()?;
193
194        Ok(Self {
195            count,
196            attributes,
197            truncate_details,
198        })
199    }
200}
201
202#[derive(Debug, Clone)]
203pub struct EdgeGroupOverview {
204    pub count: usize,
205    pub attributes: GrHashMap<GraphRecordAttribute, AttributeOverview>,
206
207    truncate_details: Option<usize>,
208}
209
210impl Display for EdgeGroupOverview {
211    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
212        let mut builder = Builder::new();
213
214        builder.push_record([
215            "Edge Count",
216            "Attribute",
217            "Attribute Type",
218            "Data Type",
219            "Details",
220        ]);
221
222        for (attribute, overview) in &self.attributes {
223            let details = overview.data.details();
224
225            builder.push_record([
226                &self.count.to_string(),
227                &attribute.to_string(),
228                overview.data.attribute_type_name(),
229                &overview.data_type.to_string(),
230                &details,
231            ]);
232        }
233
234        let mut table = builder.build();
235        table.with(Style::modern());
236        table.with(Panel::header("Edge Overview"));
237        table.with(MergeDuplicatesVerticalByColumn::new(vec![0]));
238        table.with(Alignment::center_vertical());
239        table.with(BorderCorrection {});
240
241        if let Some(truncate_details) = self.truncate_details {
242            table.modify(Columns::last(), Width::truncate(truncate_details));
243        }
244
245        writeln!(f, "{table}")
246    }
247}
248
249impl EdgeGroupOverview {
250    fn new(
251        graphrecord: &GraphRecord,
252        group_schema: &GroupSchema,
253        group: Option<&Group>,
254        truncate_details: Option<usize>,
255    ) -> OverviewResult<Self> {
256        let edges_in_group: GrHashSet<_> = match group {
257            Some(group) => graphrecord.edges_in_group(group)?.copied().collect(),
258            None => graphrecord.ungrouped_edges().copied().collect(),
259        };
260        let count = edges_in_group.len();
261
262        let attributes: GrHashMap<_, _> = group_schema
263            .edges()
264            .par_iter()
265            .map(|(key, attribute_data_type)| {
266                let attribute_type = attribute_data_type.attribute_type();
267                let data_type = attribute_data_type.data_type().clone();
268
269                let attribute_overview = match attribute_type {
270                    AttributeType::Categorical => {
271                        let selection = graphrecord.query_edges(|edges| {
272                            let edges = edges.filter(edges.index().is_in(edges_in_group.clone()));
273
274                            edges
275                                .attribute(key.clone())
276                                .on_error_of::<MissingAttribute>(Drop)
277                        });
278
279                        let values: Vec<_> = selection
280                            .evaluate()?
281                            .map(|(_, value)| value)
282                            .collect::<QueryResult<_>>()?;
283
284                        AttributeOverview {
285                            data_type,
286                            data: AttributeOverviewData::Categorical {
287                                distinct_values: values
288                                    .into_iter()
289                                    .sorted_by(GraphRecordValue::total_cmp)
290                                    .dedup()
291                                    .collect(),
292                            },
293                        }
294                    }
295                    AttributeType::Continuous => {
296                        let selection = graphrecord.query_edges(|edges| {
297                            let edges = edges.filter(edges.index().is_in(edges_in_group.clone()));
298                            let values = edges
299                                .attribute(key.clone())
300                                .on_error_of::<MissingAttribute>(Drop);
301                            let values = values.filter(!values.is_null());
302
303                            (values.min(), values.mean(), values.max())
304                        });
305
306                        let (minimum, mean, maximum) = selection.evaluate()?;
307
308                        AttributeOverview {
309                            data_type,
310                            data: AttributeOverviewData::Continuous {
311                                min: minimum.transpose()?.unwrap_or(GraphRecordValue::Null),
312                                mean: mean.transpose()?.unwrap_or(GraphRecordValue::Null),
313                                max: maximum.transpose()?.unwrap_or(GraphRecordValue::Null),
314                            },
315                        }
316                    }
317                    AttributeType::Temporal => {
318                        let selection = graphrecord.query_edges(|edges| {
319                            let edges = edges.filter(edges.index().is_in(edges_in_group.clone()));
320                            let values = edges
321                                .attribute(key.clone())
322                                .on_error_of::<MissingAttribute>(Drop);
323                            let values = values.filter(!values.is_null());
324
325                            (values.min(), values.max())
326                        });
327
328                        let (minimum, maximum) = selection.evaluate()?;
329
330                        AttributeOverview {
331                            data_type,
332                            data: AttributeOverviewData::Temporal {
333                                min: minimum.transpose()?.unwrap_or(GraphRecordValue::Null),
334                                max: maximum.transpose()?.unwrap_or(GraphRecordValue::Null),
335                            },
336                        }
337                    }
338                    AttributeType::Unstructured => {
339                        let selection = graphrecord.query_edges(|edges| {
340                            let edges = edges.filter(edges.index().is_in(edges_in_group.clone()));
341
342                            edges
343                                .attribute(key.clone())
344                                .on_error_of::<MissingAttribute>(Drop)
345                        });
346
347                        let values: Vec<_> = selection
348                            .evaluate()?
349                            .map(|(_, value)| value)
350                            .collect::<QueryResult<_>>()?;
351
352                        AttributeOverview {
353                            data_type,
354                            data: AttributeOverviewData::Unstructured {
355                                distinct_count: values
356                                    .into_iter()
357                                    .sorted_by(GraphRecordValue::total_cmp)
358                                    .dedup()
359                                    .count(),
360                            },
361                        }
362                    }
363                };
364
365                Ok((key.clone(), attribute_overview))
366            })
367            .collect::<QueryResult<_>>()?;
368
369        Ok(Self {
370            count,
371            attributes,
372            truncate_details,
373        })
374    }
375}
376
377#[derive(Debug, Clone)]
378pub struct GroupOverview {
379    pub node_overview: NodeGroupOverview,
380    pub edge_overview: EdgeGroupOverview,
381}
382
383impl Display for GroupOverview {
384    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
385        writeln!(f, "{}", self.node_overview)?;
386        writeln!(f, "{}", self.edge_overview)
387    }
388}
389
390impl GroupOverview {
391    pub(crate) fn new(
392        graphrecord: &GraphRecord,
393        group: Option<&Group>,
394        truncate_details: Option<usize>,
395    ) -> OverviewResult<Self> {
396        let schema = graphrecord.get_schema();
397
398        let group_schema = match group {
399            Some(group) => schema.group(group).map_err(GraphRecordError::from)?,
400            None => schema.ungrouped(),
401        };
402
403        Ok(Self {
404            node_overview: NodeGroupOverview::new(
405                graphrecord,
406                group_schema,
407                group,
408                truncate_details,
409            )?,
410            edge_overview: EdgeGroupOverview::new(
411                graphrecord,
412                group_schema,
413                group,
414                truncate_details,
415            )?,
416        })
417    }
418}
419
420pub trait GroupOverviewable {
421    fn group_overview(
422        &self,
423        group: &Group,
424        truncate_details: Option<usize>,
425    ) -> OverviewResult<GroupOverview>;
426}
427
428impl GroupOverviewable for GraphRecord {
429    fn group_overview(
430        &self,
431        group: &Group,
432        truncate_details: Option<usize>,
433    ) -> OverviewResult<GroupOverview> {
434        GroupOverview::new(self, Some(group), truncate_details)
435    }
436}